# Architecture overview ## The M0 architecture At the center of M0 is a shared infrastructure. Builders create **M0 Stablecoin Extensions** - custom stablecoins that inherit the security and liquidity of the underlying platform, but can be configured with their own brand, monetization model, transfer rules, and economic parameters. Stablecoin extensions are audited. They interoperate with every other M0-powered stablecoin through Onchain Orchestration. And they can be transferred cross-chain. Multiple regulated financial institutions (issuers) connect to the platform, pledge eligible collateral, and mint these application-specific stablecoins designed by builders. The result: you get the programmability and control of a custom stablecoin, without having to rebuild the monetary infrastructure from scratch. --- ## Why M0 ### For builders - Your own stablecoin, configured for your use case - not a white-label someone else controls. - Issuer portability: partner with any M0 issuer and retain the right to switch. - Shared liquidity across the M0 infrastructure from day one. - Audited, production-ready contracts as your starting point. - Trusted by PayPal, MetaMask, Kast, and others. ### For issuers - Enter stablecoin issuance without building proprietary infrastructure. - Go live in weeks, not months. - Issue across multiple blockchains and currencies from a single stack. - Near-real-time rewards distribution - Trusted by Bridge (Stripe), MoonPay, 1Money, and others. --- ::card-group :::card --- icon: i-lucide-rocket title: Design Your Stablecoin to: https://docs.m0.org/build/overview --- Start building your stablecoin on M0. ::: :::card --- icon: i-lucide-landmark title: Power Stablecoin Issuance to: https://docs.m0.org/issuers/overview --- Learn how qualified institutions connect to M0. ::: :: # About M0 The financial system runs on money designed for a different era. Base money, commercial bank money, fintech money - all of it fragmented, intermediated, and running on technology that predates the modern internet. Stablecoins were supposed to change that: money that moves as freely as information. But the first generation solved only a narrow version of the problem - a dollar on a blockchain, issued by a single company, with no real room for customization. The structures stablecoins were meant to replace simply reappeared in a new form. M0 was built to go further. We believe money should be plural. A business should be able to create the exact form of digital money its product demands, configure how that money behaves, and have it interoperate with other stablecoins out there - rather than inheriting someone else's constraints. That matters because everyone considering stablecoins as infrastructure faces the same limited options, and each path costs something you can't get back. 1. Build your own stack, and you take on massive investment, regulatory complexity, and a slow path to market. 2. Adopt someone else's stablecoin, and you get zero customization and no control over economics or UX. 3. Partner with a vertically integrated vendor, and you accept lock-in and forced bundling. M0 is the alternative: every layer is yours to configure, assign, and replace - so the money your product runs on is genuinely yours. ![Hub](https://docs.m0.org/images/the_m0_stack.png) --- ## How M0 works: the M0 stack A stablecoin's architecture has three foundational layers. M0 spans the entire lifecycle, and you choose what to build and what to plug in. **Application — design your stablecoin.** How your money behaves: who can hold and use it, what happens on interaction, how it complies, and how it's monetized. With [**Stablecoin Extensions**](https://docs.m0.org/build/overview), you start from a proven monetization template - Treasury, User Rewards, or Institution - then layer your own behavior on top with access and risk controls (including onchain screening via [Predicate](https://predicate.io/){rel=""nofollow""}) and, where templates don't fit, bespoke logic shipped as a native part of your stablecoin. **Distribution — integrate your stablecoin.** How your money reaches your use case and stays liquid. [**Onchain Orchestration**](https://docs.m0.org/build/accessing-liquidity) gives you scalable liquidity from day one (1:1 conversion against USDC), multi-chain reach without integrating bridges yourself, and interoperability with every other M0-powered stablecoin. **Issuance — issue your stablecoin.** What keeps the money worth what it should be: reserves, minting, and burning. M0 makes every path possible. Partner with a regulated issuer running a **Stablecoin Core** (issuer-based), build your stablecoin as a wrapper around onchain reserves (wrapper-based), or - depending on your use case and licensing - hold reserves and run a Stablecoin Core to become your own issuer. ::card --- icon: i-lucide-shield-check title: Stablecoin Features to: https://docs.m0.org/build/stablecoin-features --- Explore the full scope of features you can customize on your stablecoin. :: --- ## Why M0 - **Modularity** - Configure each element of the stablecoin stack independently. - **Optionality** - Change your mind, revisit designs, and upgrade partners as the business grows. - **Open architecture** - Use the full stack from the start, with the freedom to plug in specialized partners as needed. --- ## Who M0 is For ### Stablecoin Builders Builders are the application developers and product companies M0 is built for - teams embedding a branded, customized digital money instrument into their product because how their money behaves is part of their competitive advantage. They don't want to take on the infrastructure burden of building issuance from scratch, and they don't want vendor lock-in to avoid it. With M0, builders: - **Design** how their money behaves at the application layer - monetization model, access and risk controls, and bespoke logic - with Stablecoin Extensions. - **Integrate** with shared liquidity, multi-chain reach, and ecosystem-wide interoperability through Onchain Orchestration, liquid from day one. - **Issue** on the path that fits their use case and jurisdiction - partnering with an issuer powered by M0, or becoming their own. Builders stay in control of their product while M0 provides the monetary infrastructure underneath. ### Issuing Partners Issuance is a serious craft: licensing, reserves, compliance, and the trust that comes from doing all of it well. Issuing partners bring that craft to the builders who need it, running a Stablecoin Core to fulfill the regulated role only a financial institution can. For builders, a choice of issuing partners on a common standard means real optionality - the technology stack is standard, while each partner competes on the strength of its license, the quality of its reserves, the depth of its expertise, and the markets it can reach. ::card --- icon: i-lucide-rocket title: Launch a Stablecoin to: https://docs.m0.org/build/overview --- Everything you need to go from zero to a deployed, live stablecoin extension on M0. :: --- ## Explore M0 ::card-group :::card --- icon: i-lucide-rocket title: Design Your Stablecoin to: https://docs.m0.org/build/overview --- Everything you need to deploy your own stablecoin extension. ::: :::card --- icon: i-lucide-landmark title: Power Stablecoin Issuance to: https://docs.m0.org/issuers/overview --- Learn how qualified institutions connect to M0 as issuers. ::: :: # Platform mechanics The M0 platform powers builders of safe, programmable, interoperable stablecoins. It introduces a superior coordination mechanism that democratizes access to the generation and management of programmable, digital cash instruments. There are a few key areas to understand how value flows through the platform: ## Eligible Collateral With the goal of maintaining a standard issuance layer, as well as ensuring stablecoins built on M0 are interoperable, M0 indicates a list of portfolio components that are deemed eligible as reserve, or collateral, for stablecoins minted on the M0 platform. The spirit of those eligibility criteria remains that of ensuring that only the safest and regulatory aligned assets will continue to constitute the value reserve, limiting regulatory arbitrage in a scenario of international issuance. The current US regulatory landscape, thanks to the regulatory advancement brought forward by the GENIUS act, makes it much easier for the standardization of eligible collateral across multiple issuers. ## Mint Ratio The mint ratio defines the percentage of a issuer's collateral value that can be used to generate M0-powered stablecoins. It sets the required level of over-collateralization for each issuer and ensures that the system remains fully backed while accounting for operational and risk management needs. ## Minter Rate M0 is an accrual-based, rather than cash-based, system. The minter rate is a universal, continuously compounding rate applied to each issuer's outstanding issued supply on the platform. This rate is typically calibrated below the yield possibly obtained from Eligible Collateral to allow for coverage of operational costs and ensure there is a viable business for issuers. ## Earner Rate The protocol also sets an *Earner Rate* paid (only) to approved Earners on their holdings. Smart contracts ensure that value paid out via the Earner Rate does not exceed what accrued via the Minter Rate. When set equal to the Minter Rate-and 100% of holdings sit in approved earning extensions-100% of Issuer' payments flow through as rewards to Earners, continuously and programmatically. ## Automatic Fee Distribution All fees accumulate within the platform and are redistributed automatically via smart contracts. Issuers do not need to make manual payments. Instead, platform logic take care of internal accounting (indexing) and ensure that all risk and reward parameters flow consistently across all the different environments where M0-powered stablecoins exist. The superiority of an on-chain native, rather than off-chain manual, reward streaming system grows exponentially with the nesting of reward distribution logics, and the omni-chain expansion of all those use cases. Effectively, universal indexing allows the seamless management of exponentially complex reward streaming configuration. ## Extension Yield Allocation Each stablecoin extension defines how yield is allocated-whether directly to a treasury, to users, or through customized logic. The platform continuously streams rewards on-chain, allowing balances to grow incrementally instead of through periodic payouts. This transparency and programmability enable flexible product designs, such as real-time savings or incentive systems tailored to each partner's needs. # Liquidity A key benefit of the M0 infrastructure is the shared liquidity among all stablecoin extensions. Rather than relying on fragmented third-party aggregators or static onchain liquidity pools, M0 provides a unified liquidity layer that gives your stablecoin extension the most flexible and lowest friction access to liquidity from day one. ## What is the Onchain Orchestration service? Onchain Orchestration is M0's infrastructure for facilitating swaps and cross-chain transfers between M0 stablecoin extensions and other instruments. It enables users to move between a supported quote token (currently USDC) and any M0-powered stablecoin via deterministic 1:1 swaps through a unified integration surface. The product: - **Aggregates multiple liquidity sources** built specifically for M0-powered stablecoins behind a single API. - **Supports cross-chain transfers** between any configured chains and across any M0-powered stablecoin extension. - **Enables any transaction size** through partial fills and solver inventory cycling. - **Leverages M0's balance sheet** so customers are able to partially bootstrap liquidity. ## How It Works The liquidity flow works in four stages: 1. **Client creates an order** through the [Orchestration API](https://docs.m0.org/api-reference/orchestration/overview). 2. **Market Maker fills the order** using their onchain balance. 3. **Market Maker rebalances** with the Issuer as needed. 4. **Issuer balances their backbook** to replenish inventory. ## Onchain Orchestration Key Components ### Orchestration API The [Orchestration API](https://docs.m0.org/api-reference/orchestration/overview) is the primary integration point for customers. It provides a single endpoint that: - **Returns quotes** for moving between M0 stablecoin extensions and supported external stablecoins such as USDC. - **Aggregates routes** from multiple providers (M0 Portals, M0 Swap Facility, Limit Order Protocol, third-party bridges and DEXs). - **Generates executable transaction payloads** for both EVM and Solana. - **Abstracts away** the complexity of multi-chain routing. ### Limit Order Protocol The [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol) is the core settlement layer for moving in and out of stablecoins powered by M0 from other major stablecoins. It allows users to submit same-chain or cross-chain limit orders that are filled by approved solvers. Key features: - **Deterministic pricing** - clients specify exact input and output amounts upfront. - **Partial fills** - solvers can cycle inventory for large orders. - **Cross-chain support** - orders created on the source chain, filled on any destination. - **Separate funder and owner** - a wrapper or router contract can fund an order on behalf of an end user, who owns the order and receives any refund. - **Destination-chain cancellation** - cancels are initiated on the destination chain. Before the fill deadline, a same-chain order can be cancelled by its owner or its recipient; a cross-chain order only by the destination recipient. - **Permissionless refunds** - after an order's fill deadline, anyone can cancel it to release the unfilled remainder back to its owner. - **Multi-chain support** across major EVM chains and Solana. ### M0 Swap Facility (MSF) M0 smart contracts that handle interoperability between M0 stablecoin extensions, enabling direct conversions between any two approved M0 stablecoin extensions. Key features: - **Direct 1:1 conversions** between any two M0 stablecoin extensions, with no slippage, trading fees, or AMM pools required. - **Permissionless for end users** to swap between standard stablecoin extensions. - **Restricted to approved extensions** on the M0 earner list, with configuration managed by M0. - **Atomic conversions** - they either complete fully or revert. ### Portal V2 [Portal V2](https://docs.m0.org/protocol/portal-v2) is M0's cross-chain bridge and messaging system. It underpins all cross-chain transfers of M0 stablecoin extension and serves as the trusted message relay that the Limit Order Protocol depends on to deliver fill and cancel reports between chains. Portal V2 uses a modular bridge adapter design - each adapter implements a standard interface and handles message delivery via a specific cross-chain messaging protocol. It currently supports adapters for **Wormhole**, **Hyperlane**, and **LayerZero**. ## Primary Market Liquidity (Direct with Issuers) For large-scale minting/redeeming of your stablecoin extension directly against supported stablecoins or USD. This involves an onboarding process with the Issuer of the desired stablecoin. Suitable for centralized businesses and zero-slippage constraints. ## Onchain Liquidity (DEXs) Through deep liquidity pools on major chains like Ethereum, Solana, and various L2s. These pools are typically against USDC and aim for efficient pricing. Ideal for decentralized applications and 24/7 access. | DEX Name | Pool Link | | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Uniswap | [ethereum/0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288](https://app.uniswap.org/explore/pools/ethereum/0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288){rel=""nofollow""} | | Raydium | [pool\_id=CsMzKUUJNoAoU7N4zh3hAS6qcByU81TcQMPJqCdmmcEF](https://raydium.io/clmm/create-position/?pool_id=CsMzKUUJNoAoU7N4zh3hAS6qcByU81TcQMPJqCdmmcEF){rel=""nofollow""} | ## Supported Routes The Orchestration API supports same-chain swaps and cross-chain transfers between M0 stablecoin extensions and supported external stablecoins (currently USDC). Coverage is **per liquidity source**, not uniform across the network. Each source handles a different kind of route on a different set of chains, and a single quote may combine more than one: | Liquidity source | Route type | Where it's available | | ------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol) | Same-chain **and** cross-chain, any supported pair | Chains with an `OrderBook` deployment — see [deployments](https://docs.m0.org/protocol/limit-order-protocol#deployments) | | M0 Swap Facility | Same-chain only, extension ↔ extension | Any chain where two or more M0 extensions are live | | [Portal V2](https://docs.m0.org/protocol/portal-v2) | Cross-chain transfer of a single extension | Any chain that extension is deployed on | | Third-party bridges and DEXs | Varies by provider | Used to complete a route when no M0 source covers it | So a pair may be swappable on one chain but only bridgeable on another, and the limit order network is deliberately narrower than the set of chains M0 extensions reach. ::note **Check the API, not this page, before you build a route.** Chain and asset coverage changes regularly. The [`/supported-assets`](https://docs.m0.org/api-reference/orchestration/supported-assets) endpoint is the authoritative list of what is live right now, and [`/quote`](https://docs.m0.org/api-reference/orchestration/quote) tells you which source will actually serve a given pair. :: ## Use Cases for Stablecoin Builders ### Inflows: Converting USDC to Your Stablecoin Extension Users can convert USDC into your M0 stablecoin extension 1:1 through the Orchestration API. The system handles routing through the optimal path - whether that's direct swaps, cross-chain bridges, or the limit order system. ### Outflows: Converting Your Stablecoin Extension to USDC Users can exit your extension back to USDC 1:1. The solver network ensures liquidity is available even for large redemptions, with the minter backbook providing additional depth. ### Converting Between M0 Stablecoin Extensions The Orchestration API can be used to convert your stablecoin directly into any other M0 stablecoin extension 1:1 via the M0 Swap Facility. ### Cross-Chain Transfers Move your stablecoin extension between supported chains. The system automatically handles bridging through M0's [Portal infrastructure](https://docs.m0.org/protocol/portal-v2) and third-party bridges as needed. ## Next Steps ::card-group :::card --- icon: i-lucide-gavel title: Limit Order Protocol to: https://docs.m0.org/protocol/limit-order-protocol --- Deep dive into the onchain settlement layer for cross-chain swaps. ::: :::card --- icon: i-lucide-route title: Portal V2 to: https://docs.m0.org/protocol/portal-v2 --- Technical documentation for M0's cross-chain bridge and messaging system. ::: :::card --- icon: i-lucide-code title: Orchestration API to: https://docs.m0.org/api-reference/orchestration/overview --- Full API reference for requesting quotes and transaction payloads. ::: :::card --- icon: i-lucide-link title: Cross-Chain to: https://docs.m0.org/build/cross-chain --- Guide to deploying and bridging stablecoins across networks. ::: :: # Integrating with Portal V2 This guide is for developers integrating with [Portal V2](https://docs.m0.org/protocol/portal-v2), M0's unified cross-chain bridge. Portal V2 replaces the previous two-protocol system (Portal Standard on Wormhole NTT and Portal Lite on Hyperlane) with a single contract deployed on every chain and a pluggable bridge adapter layer. - **Source code**: [m-portal-v2 on GitHub](https://github.com/m0-foundation/m-portal-v2){rel=""nofollow""} - **Architecture**: [Portal V2](https://docs.m0.org/protocol/portal-v2) ## Key concepts - **One contract per chain.** `HubPortal` on Ethereum locks and releases tokens. `SpokePortal` on every other chain mints and burns them. You always call the Portal on the source chain. - **Unified flow.** The same `quote`, `approve`, `sendToken` sequence works hub to spoke, spoke to hub, and spoke to spoke (where cross-spoke transfers are enabled). - **Standard EVM chain IDs.** `destinationChainId` is the destination chain's standard EVM chain ID (`uint32`). The bridge adapter maps it to the provider-specific ID internally. There are no Wormhole chain IDs in Portal V2. - **`bytes32` addressing.** `recipient`, `destinationToken`, and `refundAddress` are `bytes32`. Left-pad a 20-byte EVM address to 32 bytes (for example, viem's `pad`). Solana addresses are already 32 bytes. - **Fee as `msg.value`.** Cross-chain delivery costs a fee. Read it with `quote` and pass it as the payable value on `sendToken`. Excess is returned to `refundAddress`. - **Pluggable adapters.** Each destination chain has a default bridge adapter (Wormhole, Hyperlane, or LayerZero). You can optionally specify an explicit adapter using the overloaded functions. - **Wrap-failure fallback.** If the destination token is an extension and wrapping fails, the recipient receives the underlying `$M` token instead of the transaction reverting. ::note Portal V2's `sendToken` handles `$M`, Wrapped `$M`, and M0 extension tokens. Set `sourceToken` and `destinationToken` to whichever pair you are bridging. :: ## Supported chains Portal V2 is live on Ethereum (the hub) and the spokes below. Pass the destination's standard EVM chain ID as `destinationChainId`; the adapter maps it to the provider ID internally. | Chain | EVM chain ID | | -------------- | ------------ | | Ethereum (hub) | `1` | | Arbitrum | `42161` | | Base | `8453` | | Linea | `59144` | | BNB | `56` | | HyperEVM | `999` | | Mantra | `5888` | | Soneium | `1868` | | Plasma | `9745` | | Citrea | `4114` | | 0G | `16661` | | Fluent | `25363` | | Moca | `2288` | | Monad | `143` | | Rise | `4153` | | Nexus | `3946` | Read the per-chain `HubPortal` / `SpokePortal` and token addresses from the [Deployments page](https://docs.m0.org/resources/addresses/m0-platform). Solana is a non-EVM (SVM) chain; it is a supported spoke but uses SVM addressing, so see the [Solana docs](https://docs.m0.org/protocol/solana) for SVM-specific integration. ## Addresses Do not hard-code Portal or token addresses. Read the current `HubPortal`, `SpokePortal`, `$M`, and `w$M` addresses from the [Deployments page](https://docs.m0.org/resources/addresses/m0-platform). ## Contract interface ### `quote` Returns the fee (in the source chain's native currency) for delivering a message to the destination chain. Pass this value as `msg.value` on the sending call. ```solidity function quote( uint32 destinationChainId, PayloadType payloadType ) external view returns (uint256); // Overload to quote a specific bridge adapter: function quote( uint32 destinationChainId, PayloadType payloadType, address bridgeAdapter ) external view returns (uint256); ``` `payloadType` is `0` for a token transfer (`TokenTransfer`). The other payload types (`Index`, `RegistrarKey`, `RegistrarList`, `FillReport`, `EarnerMerkleRoot`, `CancelReport`) are used for protocol metadata and OrderBook messages, not user token bridging. ### `sendToken` Locks or burns the source token and sends a cross-chain message that mints or releases the destination token to `recipient`. ```solidity function sendToken( uint256 amount, address sourceToken, uint32 destinationChainId, bytes32 destinationToken, bytes32 recipient, bytes32 refundAddress, bytes calldata bridgeAdapterArgs ) external payable returns (bytes32 messageId); // Overload to route through a specific bridge adapter: function sendToken( uint256 amount, address sourceToken, uint32 destinationChainId, bytes32 destinationToken, bytes32 recipient, bytes32 refundAddress, address bridgeAdapter, bytes calldata bridgeAdapterArgs ) external payable returns (bytes32 messageId); ``` Parameters: - `amount`: token amount in base units (`$M` and `w$M` use 6 decimals). - `sourceToken`: the token address on the source chain (`$M`, `w$M`, or an extension). - `destinationChainId`: the destination chain's standard EVM chain ID. - `destinationToken`: the `bytes32` token address to receive on the destination chain. - `recipient`: the `bytes32` address that receives the tokens. - `refundAddress`: the `bytes32` address that receives any excess native fee. - `bridgeAdapter` (overload only): an explicit bridge adapter address. Omit to use the chain's default adapter. - `bridgeAdapterArgs`: optional adapter arguments; pass empty bytes (`0x`) for default behavior. The first overload uses the default bridge adapter configured for the destination chain, which is what most integrations should use. ## Integration flow The sequence is identical in every direction. Always target the Portal on the source chain (`HubPortal` on Ethereum, `SpokePortal` elsewhere). ::steps ### Step 1: Quote the fee Call `quote(destinationChainId, 0)` on the source-chain Portal to get the native fee for a token transfer. ### Step 2: Approve the Portal Call `approve(portal, amount)` on the source token so the Portal can pull `amount`. ### Step 3: Send the token Call `sendToken(...)` on the source-chain Portal, passing the quoted fee as `msg.value` and the `bytes32`-encoded `destinationToken`, `recipient`, and `refundAddress`. ### Step 4: Track delivery `sendToken` returns a `messageId` and emits a `TokenSent` event. The bridge adapter relays the message and the destination Portal emits `TokenReceived` on arrival. :: ## TypeScript example (viem) This example bridges `$M` from Ethereum to Arbitrum using viem. Read the actual `HubPortal` and `$M` addresses from the [Deployments page](https://docs.m0.org/resources/addresses/m0-platform). ```ts import { createPublicClient, createWalletClient, http, pad, parseAbi } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { mainnet } from 'viem/chains' // Addresses: read from /resources/addresses/m0-platform const HUB_PORTAL = '0x...' as const // HubPortal on Ethereum const M_TOKEN = '0x...' as const // $M on Ethereum const M_TOKEN_ARBITRUM = '0x...' as const // $M on Arbitrum const ARBITRUM_CHAIN_ID = 42161 // standard EVM chain ID const PAYLOAD_TYPE_TOKEN_TRANSFER = 0 const portalAbi = parseAbi([ 'function quote(uint32 destinationChainId, uint8 payloadType) view returns (uint256)', 'function sendToken(uint256 amount, address sourceToken, uint32 destinationChainId, bytes32 destinationToken, bytes32 recipient, bytes32 refundAddress, bytes bridgeAdapterArgs) payable returns (bytes32)' ]) const erc20Abi = parseAbi([ 'function approve(address spender, uint256 amount) returns (bool)' ]) const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) const publicClient = createPublicClient({ chain: mainnet, transport: http() }) const walletClient = createWalletClient({ account, chain: mainnet, transport: http() }) async function bridgeMToArbitrum(amount: bigint) { // 1. Quote the messaging fee (native value to send with sendToken) const fee = await publicClient.readContract({ address: HUB_PORTAL, abi: portalAbi, functionName: 'quote', args: [ARBITRUM_CHAIN_ID, PAYLOAD_TYPE_TOKEN_TRANSFER] }) // 2. Approve the Portal to pull the tokens const approveHash = await walletClient.writeContract({ address: M_TOKEN, abi: erc20Abi, functionName: 'approve', args: [HUB_PORTAL, amount] }) await publicClient.waitForTransactionReceipt({ hash: approveHash }) // 3. bytes32-encode the destination token, recipient, and refund address const destinationToken = pad(M_TOKEN_ARBITRUM) const recipient = pad(account.address) const refundAddress = pad(account.address) // 4. Send the token, passing the quoted fee as msg.value const sendHash = await walletClient.writeContract({ address: HUB_PORTAL, abi: portalAbi, functionName: 'sendToken', args: [ amount, M_TOKEN, ARBITRUM_CHAIN_ID, destinationToken, recipient, refundAddress, '0x' // empty bridgeAdapterArgs: use the default adapter ], value: fee }) return publicClient.waitForTransactionReceipt({ hash: sendHash }) } ``` To bridge in the reverse direction (Arbitrum to Ethereum) or between spokes, point the clients at the source chain, use that chain's `SpokePortal` and token addresses, and set `destinationChainId` to the destination chain's standard EVM chain ID (for example, `1` for Ethereum). The call sequence does not change. ## Related - [Portal V2](https://docs.m0.org/protocol/portal-v2) - Architecture, message types, roles, and security model. - [Deployments](https://docs.m0.org/resources/addresses/m0-platform) - Current Portal and token addresses per chain. - [Bridging M and wM Tokens](https://docs.m0.org/build/bridging-user) - Manual walkthrough using a block explorer. - [Solana](https://docs.m0.org/protocol/solana) - Bridging to and from Solana. # Bridging M and wM Tokens This guide explains how to manually bridge `$M` and Wrapped `$M` (`w$M`) tokens across chains using [Portal V2](https://docs.m0.org/protocol/portal-v2) and a block explorer's "Write as Proxy" interface. Portal V2 is M0's unified cross-chain bridge. It replaces the previous two-portal system (Portal Standard on Wormhole NTT and Portal Lite on Hyperlane) with a single contract on every chain. The same three-step flow (`quote`, `approve`, `sendToken`) works in every direction: from the hub to a spoke, from a spoke back to the hub, and between spokes where cross-spoke transfers are enabled. ::note Portal V2 also bridges M0 extension tokens. The steps below use `$M` and `w$M`, but the same flow applies when the source or destination token is an extension. If wrapping to an extension fails on the destination chain, the recipient receives the underlying `$M` token instead. :: ## Prerequisites - A Web3 wallet (for example, MetaMask) connected to the source chain. - The wallet funded with the token you want to bridge. - Native currency on the source chain to pay for gas and the messaging fee (for example, ETH on Ethereum). ## How Portal V2 addressing works Every chain runs one Portal contract: - `HubPortal` on Ethereum locks tokens when you bridge out and releases them when you bridge back. - `SpokePortal` on every other chain mints tokens when they arrive and burns them when you bridge away. You always interact with the Portal on the chain you are sending from. A few things differ from the old system: - **Standard EVM chain IDs.** The `destinationChainId` is the destination chain's standard EVM chain ID (for example, Ethereum is `1`), not a Wormhole chain ID. The Portal's bridge adapter maps it to the underlying provider internally. - **`bytes32` addresses.** The `recipient`, `destinationToken`, and `refundAddress` fields are `bytes32`, not `address`. Left-pad a 20-byte EVM address to 32 bytes by prefixing it with `0x` followed by 24 zeros (12 bytes), then the 40 hex characters of the address. For example, address `0xABCD...1234` becomes `0x000000000000000000000000ABCD...1234`. - **A messaging fee.** Cross-chain delivery costs a fee that you pass as native value on the `sendToken` call. Read it from `quote` first. ## Supported chains Portal V2 is live on the chains below. Ethereum is the hub; the rest are spokes. When a step asks for a `destinationChainId`, use the destination's EVM chain ID from this table. | Chain | EVM chain ID | | -------------- | ------------ | | Ethereum (hub) | `1` | | Arbitrum | `42161` | | Base | `8453` | | Linea | `59144` | | BNB | `56` | | HyperEVM | `999` | | Mantra | `5888` | | Soneium | `1868` | | Plasma | `9745` | | Citrea | `4114` | | 0G | `16661` | | Fluent | `25363` | | Moca | `2288` | | Monad | `143` | | Rise | `4153` | | Nexus | `3946` | Look up each chain's `HubPortal` / `SpokePortal` and token addresses on the [Deployments page](https://docs.m0.org/resources/addresses/m0-platform). Solana is a non-EVM (SVM) chain. It is supported as a spoke, but the addressing and tooling differ. See the [Solana docs](https://docs.m0.org/protocol/solana) for Solana-specific steps. ## Addresses Do not hard-code addresses from this guide. Look up the current `HubPortal`, `SpokePortal`, `$M`, and `w$M` addresses for each chain on the [Deployments page](https://docs.m0.org/resources/addresses/m0-platform). ::note `$M` and `w$M` use 6 decimal places. When entering an amount in a contract call, add 6 zeros. To transfer 10 tokens, enter `10000000`. :: ## Bridging with Write as Proxy The flow below is identical whether you are bridging from Ethereum to a spoke, from a spoke to Ethereum, or between two spokes. Perform every step on the source chain's block explorer (for example, Etherscan for Ethereum, Arbiscan for Arbitrum). ::steps ### Step 1: Open the source-chain Portal in Write as Proxy Look up the Portal address for your source chain on the [Deployments page](https://docs.m0.org/resources/addresses/m0-platform). On Ethereum this is the `HubPortal`; on every other chain it is the `SpokePortal`. Open that address on the source chain's block explorer, go to the **Contract** tab, then **Write as Proxy**, and connect your wallet. ### Step 2: Read the fee with `quote` On the **Read as Proxy** tab, call `quote`: - `destinationChainId`: the destination chain's standard EVM chain ID (for example, `1` for Ethereum, `42161` for Arbitrum). - `payloadType`: `0` (TokenTransfer). Note the returned value. This is the messaging fee in the source chain's native currency (in wei). You will pass it as the payable value in Step 4. ### Step 3: Approve the Portal to spend your tokens Open the token you are bridging (`$M` or `w$M`) on the source chain's block explorer, go to **Write** (or **Write as Proxy**), and call `approve`: - `spender`: the Portal address from Step 1. - `amount`: the amount to bridge, in base units (6 decimals; see the note above). Confirm the transaction and wait for it to be mined. ### Step 4: Call `sendToken` Return to the Portal's **Write as Proxy** tab and call `sendToken`: - `payableAmount` (the native value sent with the call): the fee returned by `quote` in Step 2. - `amount`: the amount to bridge, in base units. - `sourceToken`: the address of the token on the source chain (`$M` or `w$M`). - `destinationChainId`: the destination chain's standard EVM chain ID. - `destinationToken`: the `bytes32` address of the token you want to receive on the destination chain. - `recipient`: the `bytes32` address that receives the tokens on the destination chain. - `refundAddress`: the `bytes32` address that receives any leftover native fee on the source chain. - `bridgeAdapterArgs`: leave empty (`0x`) to use the default bridge adapter for the destination chain. Confirm the transaction. The Portal locks or burns your tokens on the source chain and emits a `TokenSent` event with a `messageId`. ### Step 5: Wait for delivery The selected bridge adapter relays the message to the destination chain, where the Portal mints or releases the tokens to your recipient. Delivery time depends on the underlying bridge provider. You can track the destination Portal for a matching `TokenReceived` event. :: ::tip Any native fee you overpay is returned to the `refundAddress` you provided, so a slightly high `quote` value is safe. :: ## Related - [Portal V2](https://docs.m0.org/protocol/portal-v2) - Architecture, message types, and security model. - [Deployments](https://docs.m0.org/resources/addresses/m0-platform) - Current Portal and token addresses per chain. - [Integrating with Portal V2](https://docs.m0.org/build/bridging-developer) - Developer guide with contract calls and a code example. - [Solana](https://docs.m0.org/protocol/solana) - Bridging to and from Solana. # Design templates M0 provides powerful, battle-tested templates to get you to market faster. These models are audited and have been used by dozens of builders for the most common use cases. Each one offers a different approach to handling the underlying rewards generated by eligible collateral, giving you the flexibility to build the exact product you need. ::tabs :::tabs-item{label="EVM Models"} ::::card --- icon: i-lucide-piggy-bank title: MYieldToOne to: https://docs.m0.org/build/treasury-model --- ## Treasury Model **For US / Regulated Issuance** All accrued rewards flow to a single designated treasury wallet. Simple, direct, and easy to reason about. **Good for:** Loyalty programs, B2B payment products, treasury management tools. Creates an ERC-20 token backed 1:1 by eligible collateral. All rewards are claimed by calling `claimYield()`, which mints new tokens to a single designated recipient. - **Rewards Claiming:** Anyone can call `claimYield()` to mint accumulated rewards to the configured recipient. - **Access Control:** Role-based permissions for managing the recipient address and freezing accounts. - **Source Code:** `MYieldToOne.sol` :::: ::::card --- icon: i-lucide-rocket title: JMIExtension to: https://docs.m0.org/build/treasury-jmi-overview --- ### Multi-Collateral Model **Just Mint It - For Offshore Issuance** JMIExtension inherits all the functionality of MYieldToOne - rewards flow to a single treasury wallet - with one important addition: users can mint your stablecoin extension instantly against whitelisted stablecoins such as USDC and USDT. This makes onboarding significantly faster and simpler for end users. **Good for:** Offshore stablecoin launches where frictionless user onboarding is a priority. - **Instant Minting:** Users mint your stablecoin directly against whitelisted stablecoins (USDC, USDT, DAI). - **Configurable Caps:** Set per-collateral limits to manage risk. - **All MYieldToOne Features:** Inherits rewards claiming, access control, and freezing capabilities. :::: ::: :: ::tabs-item{label="SVM Models"} :::card --- icon: i-lucide-piggy-bank title: NoYield to: https://docs.m0.org/build/svm-noyield-guide-overview --- ## Treasury Model Creates a stablecoin backed 1:1 by eligible collateral on Solana. The admin claims all accumulated rewards via the `claim_fees` instruction. **Good for:** Protocol treasuries and ecosystem funds on Solana. - **Yield Claiming:** Only the admin can call `claim_fees` to mint accumulated rewards to a chosen recipient. - **Token Standard:** Uses Token-2022 (or legacy Token Program for compatibility). - **Source Code:** [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} ::: :: \:: ## Choosing Your Path 1. **Choose your issuance geography:** - **US / Regulated** → Treasury Model (`MYieldToOne` ) - **Offshore** → Multi-Collateral Model (`JMIExtension` - recommended, or `MYieldToOne` ) 2. **Choose your chain**based on your ecosystem: - **EVM chains** for Ethereum, Base, Optimism, and other supported L2s. - **SVM chains** for Solana, Fogo, or other SVM-compatible networks. 3. **Review implementation details** by reading the deep-dive documentation for your chosen implementation. Ready to build? Check out the deployment guides linked above to get started. # Cross-Chain ## M0 on EVM M0 is designed for a multichain world, enabling M0 extensions to be accessible across various blockchain ecosystems while maintaining Ethereum as the authoritative source for governance. ### Portal V2 Portal V2 is M0's bridging infrastructure that transfers M extensions across supported chains. It locks or burns tokens on the source chain and mints or unlocks them on the destination chain, enabling secure 1:1 transfers. Messages travel through modular bridge adapters rather than a single messaging protocol. Each adapter passes raw, byte-encoded Portal messages through its provider's base messaging layer. The following adapters are currently supported: - **Wormhole** — messages are published through the [Core Bridge](https://wormhole.com/docs/protocol/infrastructure/core-contracts/){rel=""nofollow""} contract and delivered by the [Executor](https://wormhole.com/docs/protocol/infrastructure/relayers/executor-framework/){rel=""nofollow""} - **Hyperlane** — messages are dispatched and delivered through the [Mailbox](https://docs.hyperlane.xyz/docs/protocol/core/mailbox){rel=""nofollow""} contract - **LayerZero** — messages are sent and received through the [Endpoint](https://docs.layerzero.network/v2/concepts/protocol/layerzero-endpoint#layerzero-endpoint){rel=""nofollow""} contract If your desired blockchain is not yet supported through these providers, please reach out to discuss custom integration possibilities. **Deeper Dive**: [Portal V2 (Cross-Chain Architecture)](https://docs.m0.org/protocol/portal-v2) ## M0 on Solana The M0 platform extends its reach beyond EVM chains by offering a native deployment on the Solana blockchain. This provides the Solana ecosystem with direct access to secure stablecoin building blocks, unlocking new possibilities for high-performance DeFi, payments, and other applications. This is a native representation, inheriting its core properties of security and rewards accrual while leveraging Solana's speed and low transaction costs. ### How It Works: A High-Level View [Portal V2](https://docs.m0.org/protocol/portal-v2) allows bridging of M0 extensions to Solana via its **Wormhole** adapter, following the same hub-and-spoke model used for EVM chains: 1. **Lock on Ethereum**: To move extensions to Solana, the native token is locked in the `HubPortal` contract on Ethereum. 2. **Message with Wormhole**: A secure message is sent via the Wormhole network to Solana. 3. **Mint on Solana**: The Portal program on Solana receives the message and mints an equivalent amount of the native Solana extension (an SPL token). This process ensures that every extension on Solana is fully backed by a corresponding token locked on Ethereum, maintaining the integrity of the total supply. ### Key Features for Solana Users & Developers - **Native Rewards**: Stablecoin extensions on Solana are designed to be rewards-accruing. The earning index from Ethereum is propagated via Wormhole, allowing Solana holders to benefit from the same underlying collateral rewards. - **High Performance**: Leverage Solana's sub-second finality and low fees for transactions. - **Deep Integration**: Extensions can be native SPL tokens or follow Token-2022 for more advanced functionality. This means that extensions can be seamlessly integrated into Solana's vibrant DeFi ecosystem, including DEXs, lending protocols, and derivatives platforms. ### Program Addresses You can find the relevant Solana program and token addresses in our central address registry: - [Solana Mainnet & Devnet Addresses](https://docs.m0.org/resources/addresses/m0-platform#solana) # Overview You want your own stablecoin. Your brand, your monetization model, your rules. M0 makes that possible without asking you to rebuild the entire monetary stack. **M0 Stablecoin Extensions** is a framework for custom stablecoins you deploy on top of the M0 platform. They inherit the platform's security, collateral guarantees, and shared liquidity - and you configure the rest: how rewards are distributed, who can hold and transfer, what the token is called. You don't need to build issuance infrastructure. You don't need to manage reserves. You partner with a licensed M0 Issuer who handles that responsibility, while you stay in control of the product. ::steps{level="3"} ### Decide on geographic issuance Before you write a line of code, talk to the M0 team. We'll help you understand which model fits your use case, which issuers you can partner with, and what the path to earner approval looks like. **For US issuance:** Get in touch so we can review your situation and walk through next steps together. **For offshore issuance:** We recommend starting with our Just Mint It (JMI) template - it's purpose-built for ease of onboarding and the fastest path to a live stablecoin. That said, you're free to use any of our other audited extension template contracts depending on your monetization model and use case. Current issuers powered by M0 include Bridge (Stripe), MoonPay, 1Money, and MXON (Offshore). [Get in Touch →](https://www.m0.org/contact-us){rel=""nofollow""} ### Choose your stablecoin extension template M0 offers two core audited, battle-tested stablecoin extension templates. The right one depends on your use case and context. :::card-group ::::card --- title: Treasury (MYieldToOne) to: https://docs.m0.org/build/treasury-model --- All accrued rewards flow to a single designated treasury wallet. Simple, direct, and easy to reason about. :::: ::::card --- title: Multi-Collateral (JMI) to: https://docs.m0.org/build/treasury-jmi-overview --- Inherits Treasury functionality with instant minting against whitelisted stablecoins like USDC and USDT. :::: ::: :: \:: [Models overview and comparison →](https://docs.m0.org/build/choosing-your-model) ### Customizing your stablecoin extension The M0 platform is designed to be flexible. The main areas builders typically configure are: 1. **Purpose & Branding** - Define your stablecoin's use case and whether it's publicly branded or an internal infrastructure component. 2. **Access Control** - Choose whether your extension is held and transferred permissionlessly or restricted to a whitelist. 3. **Rewards Distribution** - Decide how rewards flow: to a treasury, split between multiple parties, to token holders. 4. **Compliance Features** - M0 supports [Predicate](https://predicate.io/){rel=""nofollow""} integration as the preferred compliance partner on all EVM chains. 5. **Multi-Chain Deployment** - Extensions can be deployed across multiple blockchains. 6. **Advanced Rewards Management** *(Optional)* - Whitelist eligible rewards recipients, define multiple tiers, or redirect rewards. ::warning Any customization beyond the standard models requires an independent audit before deployment. Fill in the [intake form](https://www.m0.org/intake-form){rel=""nofollow""} and we'll advise on next steps. :: ### Choose your target chain(s) M0's contracts are deployed on the following blockchains: **EVM:** Ethereum, Arbitrum, Base, Optimism, Linea, BNB, HyperEVM, Mantra, Soneium, Plasma, Citrea, Nexus. **Non-EVM:** Solana (Mainnet + Devnet). M0 supports the following bridge providers: **LayerZero**, **Hyperlane**, and **Wormhole**. ::note Deploying on additional chains beyond your core choice may incur additional costs. If the chain you need isn't on this list, [get in touch](https://www.m0.org/contact-us){rel=""nofollow""} with our BD team to discuss terms and timeline for a custom deployment. :: [Full contract address list →](https://docs.m0.org/resources/addresses) ### Deploy your stablecoin extension Once you've chosen your model and target chains, follow the deployment guide: - [Treasury (MYieldToOne) deployment guide →](https://docs.m0.org/build/treasury-guide) - [Multi-Collateral (JMIExtension) deployment guide →](https://docs.m0.org/build/treasury-jmi-guide) - [Treasury (SVM) deployment guide →](https://docs.m0.org/build/svm-noyield-guide) ::warning Our contracts are audited and battle-tested. If you need custom behavior beyond the standard models, fill in the [intake form](https://www.m0.org/intake-form){rel=""nofollow""} first. :: ### Get earner approval & enable earning For your extension to accrue rewards, its contract address must be approved as an M0 Earner through on-chain configuration. 1. Deploy your extension contract. 2. Prepare your earner approval proposal. 3. Submit via the [M0 Governance Portal](https://governance.m0.org/proposal/create){rel=""nofollow""}. 4. Approval is configured on-chain. Once configuration is confirmed, call `enableEarning()` on your deployed contract. [Full earner approval guide →](https://docs.m0.org/protocol/roles) ### Enable user access Once earning is live, users can convert into your stablecoin extension (and back) through M0's Onchain Orchestration - no custom swap interface needed. DEX aggregators, wallet providers, and integration partners handle the UX automatically. Your stablecoin extension only needs to implement two functions: `wrap()` and `unwrap()`, which are called exclusively by SwapFacility. \:: ## FAQ for builders ::accordion :::accordion-item{label="Do I need smart contract experience? "} Yes. Deploying an extension requires familiarity with Solidity and EVM tooling. If you're not technical, talk to us and we'll connect you with integration support. ::: :::accordion-item{label="What if I want a model that doesn't exist yet?"} Fill in the [intake form](https://www.m0.org/intake-form){rel=""nofollow""}. Custom models are possible but require a conversation before you start building. ::: :::accordion-item{label="Is the code audited?"} All pre-built models are audited. [View audit reports →](https://docs.m0.org/resources/audits) ::: :::accordion-item{label="What chains are supported?"} Ethereum mainnet, multiple L2s, additional L1s, and Solana. M0 uses [Portal V2](https://docs.m0.org/protocol/portal-v2) for cross-chain interoperability. See the full [address list](https://docs.m0.org/resources/addresses). ::: :::accordion-item{label="How do I get liquidity from day one?"} Shared liquidity is a core benefit of building on M0. When your extension is approved, it's connected to the SwapFacility and has access to the ecosystem's liquidity on Mainnet or Solana from launch. ::: :: ## Related ::card-group :::card --- icon: i-lucide-link title: Cross-chain to: https://docs.m0.org/build/cross-chain --- Deploy and bridge stablecoins across multiple networks. ::: :::card --- icon: i-lucide-droplets title: Accessing Liquidity to: https://docs.m0.org/build/accessing-liquidity --- Connect to liquidity sources in the M0 ecosystem. ::: :: # Deploy a PYUSDx Extension (Sepolia) Deploy your own branded, yield-bearing PYUSDx extension on the Sepolia testnet. By the end you will have a branded ERC-20 token, backed 1:1 by PYUSDx, that earns yield on its underlying PYUSDx balance. You can deploy either from the **Etherscan "Write Contract" interface** (no local tooling) or with a **Foundry script**. ## Overview PYUSDx extensions let partners create branded stablecoins backed by **PYUSDx**. Your extension will: - **Wrap PYUSDx** 1:1 into your branded token (e.g. "Acme Yield USD"). - **Earn yield** on the underlying PYUSDx balance (once enabled by the M0 earner manager). - **Distribute yield** to your treasury (`yieldRecipient`) while users keep their principal. - **Provide full liquidity**: anyone can swap PYUSDx ↔ your extension anytime through the `SwapFacility`. ## Prerequisites ### 1. Get Sepolia ETH You need ETH for gas: - [Google Cloud Sepolia Faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia){rel=""nofollow""} - [Alchemy Sepolia Faucet](https://www.alchemy.com/faucets/ethereum-sepolia){rel=""nofollow""} ### 2. Get testnet PYUSDx (via the PYUSDx Faucet) The PYUSDx Faucet is the entry point for testing on Sepolia. - **Faucet address**: [`0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26`](https://sepolia.etherscan.io/address/0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26){rel=""nofollow""} - **How to use**: open the faucet on Etherscan, go to **Contract → Write Contract**, call `requestPYUSDX(recipient)` with your address (or click **Self**), then **Write**. - **Amount**: each call sends **100 PYUSDx** (`100000000`, 6 decimals) until the faucet runs dry. ## Architecture PYUSDx and every extension use **6 decimals**. Users acquire PYUSDx, approve the `SwapFacility`, and swap into your extension. Wrapping is 1:1 and can **only** be performed by the `SwapFacility` (the extension's `wrap` / `unwrap` are `onlySwapFacility`). ::note{title="Auto-approved on deploy"} Extensions deployed through the `ExtensionFactory` are **auto-approved** in the `SwapFacility` the moment they are deployed. There is no separate "add to earners list" admin step just to enable swapping. :: ## Extension types Choose the type that matches your model. Both are deployed through the same `ExtensionFactory`. | Type | Best for | Backing | | -------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------- | | **YieldToOne** | Partners who want **all yield to flow to one treasury** address. Simple and recommended for most. | PYUSDx (1:1) | | **MultiMint** | Partners who want to back the token with **multiple assets** under per-asset caps, with yield to one recipient. | PYUSDx + approved assets (capped) | ## Deploy your extension There are two ways to deploy, and both go through the same `ExtensionFactory` and produce the **same deterministic (CREATE3) address**. Use the **Etherscan** path for a no-tooling deploy, or the **Foundry** path for a reproducible, scripted deploy. ### Path 1: Deploy via Etherscan All protocol contracts (PYUSDx, `SwapFacility`, `ExtensionFactory`) are **transparent proxies**, so use the **Contract → Write as Proxy** tab. **1. Open the ExtensionFactory.** Go to the `ExtensionFactory` on Etherscan, open **Contract → Write as Proxy**, and **Connect to Web3** with your wallet: - **ExtensionFactory**: [`0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A`](https://sepolia.etherscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} **2. Deploy your extension.** Call `deployYieldToOne` (or `deployMultiMint`). Etherscan renders the `params` tuple as individual fields. Fill them in and click **Write**. `extensionName` (string) is **not** your token name. It is a unique label that, together with your deployer address, determines the extension's deterministic (CREATE3) address. Pick something unique like `acme-yield-v1`. `deployYieldToOne` takes these `params` fields: | Field | Type | What to enter | | ----------------------- | ------- | ------------------------------------------ | | `name` | string | ERC-20 token name, e.g. `Acme Yield USD` | | `symbol` | string | ERC-20 symbol, e.g. `ayUSD` | | `yieldRecipient` | address | Treasury that receives **all** yield | | `admin` | address | `DEFAULT_ADMIN_ROLE` holder | | `freezeManager` | address | Can freeze / unfreeze accounts | | `pauser` | address | Can pause / unpause the extension | | `yieldRecipientManager` | address | Can change the `yieldRecipient` | | `versionManager` | address | Can pin / unpin the implementation version | `deployMultiMint` takes the same `params` fields, **plus** one extra: | Field | Type | What to enter | | ----------------- | ------- | ---------------------------------------------------------- | | `assetCapManager` | address | Manages per-asset deposit caps for the MultiMint extension | ::tip For quick testing you can use **your own address** for every role field (`admin`, `freezeManager`, `pauser`, `yieldRecipientManager`, `versionManager`, `assetCapManager`) and your treasury for `yieldRecipient`. **Every field must be a non-zero address** or deployment reverts (`ZeroAdmin`, `ZeroFreezeManager`, `ZeroPauser`, `ZeroYieldRecipientManager`, `ZeroVersionManager`, `ZeroYieldRecipient`, `ZeroAssetCapManager`). :: ### Path 2: Deploy via Foundry (script) ::note This path requires access to the private PYUSDX source repository. If you need it, contact the M0 team. Otherwise use the Etherscan path above, which needs no repository access. :: Prefer a reproducible, command-line deploy? The repo ships Makefile targets that deploy through the **same** `ExtensionFactory`, so you get the same deterministic (CREATE3) address as the Etherscan flow. The Sepolia factory address is already baked into `deployments/11155111.json`, so you do not need to provide it. **1. Clone the repo and install Foundry.** ```bash # install Foundry if you don't have it curl -L https://foundry.paradigm.xyz | bash && foundryup git clone && cd PYUSDx forge install ``` **2. Configure your `.env`.** Create a `.env` in the repo root with your deploy key, an RPC URL, and your extension config: ```bash # Secrets. In the make path `op run` resolves op:// references and passes plain # values through, so you can mix the two; only the private key really needs vaulting. # Not using 1Password? Use a plain value for PRIVATE_KEY too and run forge directly (see note below). PRIVATE_KEY=op://// SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/ # any Sepolia RPC ETHERSCAN_API_KEY= # only needed for --verify # Extension config EXTENSION_NAME=acme-yield-v1 # unique CREATE3 label (deployer + name -> address); NOT the token name EXTENSION_TOKEN_NAME=Acme Yield USD # ERC20 name EXTENSION_TOKEN_SYMBOL=ayUSD # ERC20 symbol YIELD_RECIPIENT=0x... # treasury that receives all yield ADMIN=0x... # DEFAULT_ADMIN_ROLE holder FREEZE_MANAGER=0x... PAUSER=0x... YIELD_RECIPIENT_MANAGER=0x... VERSION_MANAGER=0x... # MultiMint only, additionally: ASSET_CAP_MANAGER=0x... ``` ::tip As with the Etherscan flow, **every address field must be non-zero** or the deploy reverts. For quick testing you can use **your own address** for every role field and your treasury for `YIELD_RECIPIENT`. :: **3. Deploy.** ```bash # YieldToOne make deploy-yield-to-one-sepolia # MultiMint make deploy-multi-mint-sepolia ``` The script logs the implementation and proxy addresses, records the proxy in `deployments/11155111.json`, and verifies the contract on Etherscan. ::note{title="Secrets and running forge directly"} The `make` targets inject secrets with the **1Password CLI** (`op run`), so by default `PRIVATE_KEY` in `.env` is an `op://...` reference. If you do not use 1Password, put **plain values** in `.env` and run the script directly (Foundry auto-loads `.env`): `forge script script/deploy/DeployYieldToOne.s.sol:DeployYieldToOne --rpc-url sepolia --broadcast --verify` (use `DeployMultiMint.s.sol:DeployMultiMint` for MultiMint). :: ## Find your extension's address You have two ways to get the deployed proxy address (both paths produce the same address): - **Predict it (before or after):** `ExtensionFactory` → **Read as Proxy** → `getExtensionAddress(deployer, extensionName)`, using your deployer address and the exact `extensionName` you used. - **Read the receipt:** open your deploy transaction on Etherscan → **Logs** → find the `ExtensionDeployed` event; `proxy` is your extension address. Confirm it is registered: `ExtensionFactory` → **Read as Proxy** → `getExtensionType(extension)` returns `1` (YieldToOne) or `2` (MultiMint), and `isApprovedExtension(extension)` returns `true`. ## Enable yield (M0 action) Your extension can be **swapped into immediately**, but it will not **earn** PYUSDx yield until the PYUSDx **earner manager** registers it as an earner. - **What**: the earner manager calls `setAccountInfo(yourExtension, earnerRate, feeRate, claimRecipient)` on PYUSDx with `earnerRate > 0` (basis points). - **Who**: the PYUSDx `EARNER_MANAGER`. - **How**: contact the M0 team with your deployed extension address to have yield enabled. ::note This step is **optional if you only want to test wrapping/swapping**. It is **required for yield to accrue**. Verify with PYUSDx → **Read as Proxy** → `isEarning(yourExtension)` (should be `true`). :: ## Use your extension (via Etherscan) Amounts use **6 decimals**: `100` tokens = `100000000`. 1. **Get PYUSDx.** Faucet ([`0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26`](https://sepolia.etherscan.io/address/0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26){rel=""nofollow""}) → **Write Contract** → `requestPYUSDX(yourAddress)`. 2. **Approve the SwapFacility to spend your PYUSDx.** PYUSDx ([`0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d`](https://sepolia.etherscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""}) → **Write as Proxy** → `approve(spender = SwapFacility, amount)`. 3. **Swap PYUSDx → extension.** SwapFacility ([`0x0bC305e7e13113cAEd3f5486849e9518a1cC4173`](https://sepolia.etherscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""}) → **Write as Proxy** → `swapIn(extensionOut = yourExtension, amount, recipient = yourAddress)`. You receive extension tokens 1:1. 4. **Swap extension → PYUSDx.** First approve the SwapFacility **on your extension**: yourExtension → **Write as Proxy** → `approve(SwapFacility, amount)`. Then SwapFacility → `swapOut(extensionIn = yourExtension, amount, recipient = yourAddress)`. 5. **Claim yield (YieldToOne).** yourExtension → **Write as Proxy** → `claimYield()`. Accrued yield is minted to your `yieldRecipient`. ## Test cross-chain transfers (testnet) The `Portal` bridges PYUSDx and your extension between testnets over LayerZero. The testnets are peered **hub-and-spoke**: Arbitrum Sepolia, Monad Testnet, and Base Sepolia each connect to **Ethereum Sepolia** (and back). There are no direct spoke-to-spoke routes, so every transfer is to or from Ethereum Sepolia. The `Portal` has the same address on every chain: `0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7`. Testnet chain IDs are Ethereum Sepolia `11155111`, Arbitrum Sepolia `421614`, Monad Testnet `10143`, and Base Sepolia `84532`. Full per-chain address lists are on the [PYUSDx platform deployments](https://docs.m0.org/resources/addresses/pyusdx-platform) page. ### Before you bridge - **Deploy your extension on both chains.** For the recipient to receive *your* extension on the destination, it must exist there at the same address. Repeat [Deploy your extension](https://docs.m0.org/#deploy-your-extension) on the destination testnet with the **same `extensionName`** — CREATE3 gives it the same address. If your extension is not deployed on the destination, the recipient receives plain PYUSDx instead. - **Hold the token and gas on both ends.** You need the token you are bridging on the source chain, plus native gas on **both** chains: the source pays the LayerZero fee and the destination executes delivery. ### Bridge your extension (via Etherscan) Amounts use **6 decimals**: `100` tokens = `100000000`. Do all of this on the **source** chain's explorer. 1. **Quote the fee.** Portal → **Read as Proxy** → `quote(destinationChainId)`. It returns the LayerZero fee in **wei**; you pass this as the transaction value in step 3. 2. **Approve the Portal to spend your token.** yourExtension → **Write as Proxy** → `approve(spender = Portal, amount)`. 3. **Send.** Portal → **Write as Proxy** → `sendToken`, with the fee from step 1 set as the payable value (in wei): | Field | Value | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `amount` | amount in 6-decimal base units | | `sourceToken` | your extension address | | `destinationChainId` | the destination chain ID | | `destinationToken` | your extension address as **bytes32** — left-pad the 20-byte address with 12 zero bytes: `0x000000000000000000000000` + the 40-hex address | | `recipient` | the recipient address as bytes32 (same padding) | | `refundAddress` | your address as bytes32 (receives any leftover fee) | 4. **Track and confirm.** Paste the transaction hash into [LayerZero Scan (testnet)](https://testnet.layerzeroscan.com){rel=""nofollow""}; delivery takes a few minutes. On arrival the `Portal` unwraps your extension to PYUSDx, bridges it, and re-wraps into your extension for the recipient — confirm the recipient's `balanceOf` on the destination chain's explorer. ::note{title="Bridging base PYUSDx"} To bridge the base token instead of an extension, set both `sourceToken` and `destinationToken` to the PYUSDx address `0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d`. PYUSDx is deployed on every chain, so no destination deploy is needed. :: ## Verify your extension Read these (use **Read as Proxy** on each contract): | Contract | Call | Expected | | ---------------- | -------------------------- | -------------------------------------------------------------- | | Extension | `name()` / `symbol()` | Your token name / symbol | | Extension | `totalSupply()` | Sum of all holders' balances | | Extension | `yield()` | Accrued, unclaimed yield (> 0 after time passes, once earning) | | Extension | `yieldRecipient()` | Your treasury address | | ExtensionFactory | `isApprovedExtension(ext)` | `true` | | PYUSDx | `isEarning(ext)` | `true` (after M0 enables yield) | ## Common issues - **The Write tab is missing `deployYieldToOne` / `swapIn` / `approve`.** You opened the plain "Write Contract" tab on a proxy. Use **Write as Proxy** for PYUSDx, `SwapFacility`, and `ExtensionFactory`. (The faucet is the exception: plain "Write Contract".) - **`NotApprovedExtension` when swapping.** The address you passed is not a factory-deployed extension. Double-check the proxy address and `isApprovedExtension`. - **`transfer amount exceeds allowance` / swap reverts.** You did not approve the `SwapFacility` first. Approve PYUSDx (for `swapIn`) or the extension (for `swapOut`) to the `SwapFacility`. - **`yield()` is 0.** Your extension is not registered as a PYUSDx earner yet. Contact M0 to enable yield (see above), then let some time pass. - **Wrong amounts.** PYUSDx and extensions use **6 decimals**. `100` tokens = `100000000`. ## FAQ ### Do my users need any special role? **No.** Once the extension is deployed via the factory it is auto-approved, and **anyone** can swap PYUSDx ↔ your extension via the `SwapFacility`. Roles (`admin`, `freezeManager`, etc.) only govern administration of your extension. ### What's the difference between PYUSDx and an extension token? PYUSDx is the shared non-rebasing base token. Your extension is a **branded 1:1 wrapper** around PYUSDx that captures the yield on its PYUSDx backing and routes it to your `yieldRecipient`. ### Can users swap directly between two extensions? Yes. `SwapFacility.swap(tokenIn, tokenOut, amount, recipient)` accepts PYUSDx or any approved extension on either side. ### Why is the deployed address deterministic? The factory uses **CREATE3** with a salt derived from your deployer address plus `extensionName`. The same deployer and name yield the same address, so pick a unique `extensionName` per extension. ## Sepolia addresses | Contract | Address | Etherscan tab | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | PYUSDx Token | [`0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d`](https://sepolia.etherscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | Write as Proxy | | PYUSDx Faucet | [`0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26`](https://sepolia.etherscan.io/address/0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26){rel=""nofollow""} | Write Contract | | ExtensionFactory | [`0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A`](https://sepolia.etherscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | Write as Proxy | | SwapFacility | [`0x0bC305e7e13113cAEd3f5486849e9518a1cC4173`](https://sepolia.etherscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | Write as Proxy | | Portal | [`0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7`](https://sepolia.etherscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | Write / Read as Proxy | | IssuerGateway | [`0x693CC3305342B02AC1549B509a704ff944Cd9499`](https://sepolia.etherscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | Write as Proxy | # Design customization M0 empowers builders to tailor a stablecoin precisely to their application's needs, leveraging the security and rewards mechanism of the M0 platform while giving them full control over the stablecoin's behavior and features. A stablecoin extension is its own smart contract that allows one to define unique rules, branding, and value flows, all built upon M0's pre-determined templates and robust infrastructure. ## Key Design Decisions Before writing code, consider these crucial aspects. With M0 you will be able to customize your stablecoin extension to fit any needs below. Your choices will define your stablecoin's functionality and value proposition. ### 1. Purpose & Branding - What will your stablecoin be used for (e.g., payments, in-app currency, rewards, DeFi collateral)? - Will it be publicly branded (e.g., YourAppUSD) or an internal/infrastructure component? ### 2. Access Control - Will your extension be held and transferred permissionlessly (publicly accessible) or restricted to a specific whitelist of users/contracts? ### 3. Rewards Distribution - If your extension contract is approved as an M0 Earner, it will accrue rewards. How will rewards be claimed? - **As revenue to a treasury / business / foundation**? - **Split between multiple parties?** (e.g. distributors, or holders and a treasury) - **As rewards to token holders?** (e.g. via rebasing balances or a claimable mechanism) M0's design gives you [complete flexibility to implement your desired monetization flow](https://docs.m0.org/build/overview). ### 4. Compliance Features - There are compliance requirements in all stablecoin extensions. M0 supports [Predicate](https://predicate.io/){rel=""nofollow""} integration as the preferred partner on all EVM chains. You can build these directly into your extension. ### 5. Multi-Chain Deployment - What blockchains does your stablecoin need to live in? Extensions can be deployed across multiple networks, ensuring your stablecoin token can natively exist and be held on other chains as needed. ### 6. Advanced Rewards Management (Optional) - For more sophisticated use cases, partners can implement advanced controls, such as whitelisting eligible rewards recipients, defining multiple rewards tiers, or redirecting rewards from LP contracts to alternate addresses. **Deeper Dive**: [M0 Extensions](https://docs.m0.org/protocol/extensions) ## Next Steps ::card-group :::card --- icon: i-lucide-rocket title: Design Your Stablecoin to: https://docs.m0.org/build/overview --- Choose a model and deploy your own stablecoin extension. ::: :::card --- icon: i-lucide-landmark title: Integrate Your Stablecoin to: https://docs.m0.org/build/accessing-liquidity --- Source liquidity directly into your use case. ::: :: # Implementation guide ## Getting Started ::note The SVM deployment guide is currently being finalized. For immediate support deploying a NoYield extension on Solana, please [get in touch](https://www.m0.org/contact-us){rel=""nofollow""} with the M0 team. :: ### 1. Clone the Repository ```bash git clone https://github.com/m0-foundation/solana-m-extensions.git cd solana-m-extensions ``` ### 2. Setup Your Environment Follow the setup instructions in the repository's README to install Anchor, Solana CLI, and other dependencies. ### 3. Configure Your Extension Configure your extension parameters including: - **Token name and symbol** for your stablecoin. - **Admin address** that will control rewards claiming and configuration. - **Token program** choice (Token-2022 recommended, legacy Token Program supported). ### 4. Deploy and Test 1. Deploy to Solana Devnet first for testing. 2. Verify all functionality including `claim_fees` . 3. Deploy to Solana Mainnet once validated. ### 5. Enable Earning After deployment, your extension's vault must be registered as an earner. The vault then begins to accrue rewards. The admin can call `claim_fees` at any time to collect accumulated rewards. ## Next Steps - [Gaining Earner Approval](https://docs.m0.org/protocol/roles) - Get your extension approved. - [Contract Addresses](https://docs.m0.org/resources/addresses) - View deployed Solana program addresses. - [Cross-Chain Interoperability](https://docs.m0.org/build/cross-chain) - Bridge your extension across chains. # Overview Deploy a Treasury Model stablecoin on Solana using the `NoYield` extension. All accrued rewards are captured by the admin via the `claim_fees` instruction. If you haven't already, review the [**Choosing Your Model**](https://docs.m0.org/build/choosing-your-model) page for context on how NoYield fits into the M0 extension ecosystem. ## Overview The `NoYield` model on SVM creates a token backed 1:1 by eligible collateral. It is the Solana equivalent of the EVM `MYieldToOne` model - all rewards flow to the protocol admin. - **Rewards Claiming:** Only the admin can call `claim_fees` to mint accumulated rewards to a chosen recipient. - **Token Standard:** Uses Token-2022 (or legacy Token Program for compatibility). - **Source Code:** [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} # Implementation guide Deploy a Treasury Model stablecoin where 100% of rewards are captured by the use case owner. This guide covers the EVM implementation using `MYieldToOne`. If you haven't already, please review the [**Treasury Model Deep Dive**](https://docs.m0.org/build/treasury-model) for conceptual details about this model and its implementations. ::note This guide covers the standard `MYieldToOne` implementation. If you need to accept additional stablecoins (USDC, USDT, DAI), see the [**JMI Implementation Guide**](https://docs.m0.org/build/treasury-jmi-guide) instead. :: ## EVM Implementation: MYieldToOne This section covers deploying the `MYieldToOne` contract on EVM chains (Ethereum, Base, Arbitrum, etc.). ### 1. Setup Your Environment First, you'll need to clone the `m-extensions` repository and set up your development environment: 1. **Clone the repository:** ```bash git clone https://github.com/m0-foundation/m-extensions.git cd m-extensions ``` 2. **Follow the setup instructions:** Refer to the [**README.md**](https://github.com/m0-foundation/m-extensions/blob/main/README.md){rel=""nofollow""} and follow all the commands provided to install dependencies and configure your development environment. 3. **Configure Environment Variables:** Create a `.env`file in the root directory: ```bash # Private key for deployment (include 0x prefix) PRIVATE_KEY=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef # RPC URLs MAINNET_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/your-api-key SEPOLIA_RPC_URL=https://eth-sepolia.alchemyapi.io/v2/your-api-key # Etherscan API key for verification ETHERSCAN_API_KEY=your-etherscan-api-key ``` **Security Warning:** Never commit your `.env` file to version control. Add it to your `.gitignore`: ```bash echo ".env" >> .gitignore ``` ### 2. Create and Initialize Your Contract Your custom extension contract will inherit from `MYieldToOne`. The most important step is the `initialize` function, where you configure your token's name, symbol, and access control roles. Create a new file in the `src/` directory, for example, `MyTreasuryToken.sol`. ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { MYieldToOne } from "./projects/yieldToOne/MYieldToOne.sol"; contract MyTreasuryToken is MYieldToOne { /// @custom:oz-upgrades-unsafe-allow constructor constructor(address mToken_, address swapFacility_) MYieldToOne(mToken_, swapFacility_) { _disableInitializers(); } function initialize( string memory name, string memory symbol, address yieldRecipient_, address admin, address freezeManager, address yieldRecipientManager ) public initializer { MYieldToOne.initialize( name, // "My Treasury USD" symbol, // "tUSD" yieldRecipient_, // Treasury wallet address admin, // Admin multisig address freezeManager, // Freeze manager address (can be same as admin) yieldRecipientManager // Rewards recipient manager address ); } } ``` #### Key Parameters Explained: - **`name`**: The full name of your token (e.g., "My Treasury USD") - **`symbol`**: The token symbol (e.g., "tUSD") - `yieldRecipient_`: The wallet that will receive all rewards (your treasury) - **`admin`**: Address with `DEFAULT_ADMIN_ROLE` (should be a multisig) - **`freezeManager`**: Address with `FREEZE_MANAGER_ROLE` - **`yieldRecipientManager`**: Address with `YIELD_RECIPIENT_MANAGER_ROLE` **Note:** The `mToken` and `swapFacility` addresses are set in the constructor as immutable variables, not in the initializer. ### 3. Write Deployment Script Create a deployment script in the `script/` directory: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Script } from "forge-std/Script.sol"; import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { MyTreasuryToken } from "../src/MyTreasuryToken.sol"; contract DeployMyTreasuryToken is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address deployer = vm.addr(deployerPrivateKey); vm.startBroadcast(deployerPrivateKey); // Deploying a Transparent Proxy, which requires a proxy admin. address proxy = Upgrades.deployTransparentProxy( "MyTreasuryToken.sol", // Contract file name deployer, // The admin for the proxy contract itself abi.encodeCall( // The initializer call data MyTreasuryToken.initialize, ( "My Treasury USD", // name "tUSD", // symbol 0x..., // rewards recipient 0x..., // admin (DEFAULT_ADMIN_ROLE for the logic) 0x..., // freezeManager 0x... // rewards recipient manager ) ) ); vm.stopBroadcast(); console.log("MyTreasuryToken deployed at:", proxy); } } ``` ### 4. Testing Your Contract Before deployment, create comprehensive tests: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Test } from "forge-std/Test.sol"; import { MyTreasuryToken } from "../src/MyTreasuryToken.sol"; contract MyTreasuryTokenTest is Test { MyTreasuryToken token; address admin = makeAddr("admin"); address treasury = makeAddr("treasury"); address user = makeAddr("user"); function setUp() public { token = new MyTreasuryToken( address(mockMToken), // $M Token address address(mockSwapFacility) // SwapFacility address ); token.initialize( "My Treasury USD", "tUSD", treasury, // Rewards recipient admin, // Admin admin, // Freeze manager admin // Rewards recipient manager ); } function testYieldClaim() public { // Test rewards claiming functionality // Mock some rewards accrual // Call claimYield() // Assert rewards goes to treasury } function testFreezing() public { // Test freezing functionality vm.prank(admin); token.freeze(user); assertTrue(token.isFrozen(user)); } function testYieldRecipientChange() public { // Test changing rewards recipient address newTreasury = makeAddr("newTreasury"); vm.prank(admin); token.setYieldRecipient(newTreasury); assertEq(token.yieldRecipient(), newTreasury); } } ``` ### 5. Gain M0 Earner Approval For your contract to accrue rewards, its deployed address must be approved as an M0 Earner. - **Calculate Your Address:** Determine your contract's final deployment address. You can do this deterministically using `CREATE2` or by deploying it to a testnet first. - **Submit a Proposal:** Create and submit a proposal to M0 to add your contract's address to the `earners` list. For more details on this process, see the [Gaining Earner Approval](https://docs.m0.org/protocol/roles) guide. ### 6. Security & Audit Even though you are building on an audited template, any modifications or new logic should be thoroughly tested and independently audited. Security is paramount. - Write comprehensive tests for your new contract in the `test/` directory. - Run static analysis tools like Slither. - Engage with a reputable security firm for a full audit if you've made significant modifications. ### 7. Deploy & Launch #### Deploy to Testnet First ```bash # Load environment variables source .env # Deploy to Sepolia forge script script/DeployMyTreasuryToken.s.sol \ --rpc-url $SEPOLIA_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast ``` #### Deploy to Mainnet ```bash # Load environment variables source .env # Deploy to Mainnet forge script script/DeployMyTreasuryToken.s.sol \ --rpc-url $MAINNET_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast ``` #### Alternative: Hardware Wallet Deployment For production deployments, consider using a hardware wallet: ```bash forge script script/DeployMyTreasuryToken.s.sol \ --rpc-url $MAINNET_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --ledger \ --broadcast ``` #### Enable Earning After your contract is deployed and earner approval is confirmed, call the `enableEarning()` function on your deployed contract: ```solidity // Call this function to start rewards accrual myTreasuryToken.enableEarning(); ``` This will start the rewards accrual process. ### 8. Integration & Usage #### For Treasury Management 1. **Claiming Rewards:**Anyone can call the rewards claim function when surplus exists: ```solidity uint256 yieldAmount = myTreasuryToken.claimYield(); ``` 2. **Managing Rewards Recipient:**The rewards recipient manager can update where rewards go: ```solidity // Only YIELD_RECIPIENT_MANAGER_ROLE can call this myTreasuryToken.setYieldRecipient(newTreasuryAddress); ``` 3. **Freezing (if needed):** ```solidity // Only FREEZE_MANAGER_ROLE can call this myTreasuryToken.freeze(suspiciousAddress); myTreasuryToken.unfreeze(rehabilitatedAddress); // Batch operations address[] memory accounts = new address[](3); accounts[0] = addr1; accounts[1] = addr2; accounts[2] = addr3; myTreasuryToken.freezeAccounts(accounts); ``` #### Frontend Integration Display real-time rewards information to your users: ```javascript // Get current claimable rewards const currentYield = await myTreasuryToken.yield(); // Get total supply const totalSupply = await myTreasuryToken.totalSupply(); // Get user balance const userBalance = await myTreasuryToken.balanceOf(userAddress); // Check if address is frozen const isFrozen = await myTreasuryToken.isFrozen(userAddress); // Get rewards recipient const yieldRecipient = await myTreasuryToken.yieldRecipient(); ``` ### 9. Monitoring & Maintenance #### Key Metrics to Track - **Total Supply**: How much of your token is in circulation. - **Rewards Rate**: Current rewards being generated. - **Treasury Balance**: Amount accumulated in the rewards recipient address. - **Underlying Balance**: Total value held by your contract. #### Operational Considerations - Monitor the `yield()` function regularly to see claimable amounts. - Set up automated rewards claiming if desired. - Keep track of frozen addresses for compliance. - Monitor gas costs for user operations. **Congratulations!** You now have a fully functional treasury-focused stablecoin that automatically accumulates rewards for your use case while providing users with a stable, 1:1 backed token experience. ## SVM Implementation For the Solana/SVM implementation, see the dedicated [**NoYield (SVM) Deployment Guide**](https://docs.m0.org/build/svm-noyield-guide). # Implementation guide ::note The JMI extension is currently available on EVM chains only (Ethereum, Base, Arbitrum, etc.). :: This guide provides step-by-step instructions for deploying a JMI (Just Mint It) extension, where multiple collateral types are accepted and 100% of the rewards are captured by a designated recipient. If you haven't already, please review the [**JMI Deep Dive**](https://docs.m0.org/build/treasury-jmi-overview) for conceptual details about this model. ::note For single-collateral deployments, consider the simpler [**MYieldToOne implementation**](https://docs.m0.org/build/treasury-guide) instead. :: ## EVM Implementation This section covers deploying the `JMIExtension` contract on EVM chains (Ethereum, Base, Arbitrum, etc.). ### 1. Setup Your Environment First, clone the `m-extensions` repository and set up your development environment: 1. **Clone the repository:** ```bash git clone https://github.com/m0-foundation/m-extensions.git cd m-extensions ``` 2. **Follow the setup instructions:** Refer to the [**README.md**](https://github.com/m0-foundation/m-extensions/blob/main/README.md){rel=""nofollow""} and follow all commands to install dependencies and configure your development environment. 3. **Configure Environment Variables:** Create a `.env`file in the root directory: ```bash # Private key for deployment (include 0x prefix) PRIVATE_KEY=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef # RPC URLs MAINNET_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/your-api-key SEPOLIA_RPC_URL=https://eth-sepolia.alchemyapi.io/v2/your-api-key # Etherscan API key for verification ETHERSCAN_API_KEY=your-etherscan-api-key ``` **Security Warning:** Never commit your `.env` file to version control: ```bash echo ".env" >> .gitignore ``` ### 2. Create Your Contract Your custom JMI extension will inherit from `JMIExtension`. Create a new file in the `src/` directory, for example, `MyJMIToken.sol`: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { JMIExtension } from "./projects/jmi/JMIExtension.sol"; contract MyJMIToken is JMIExtension { /// @custom:oz-upgrades-unsafe-allow constructor constructor(address mToken_, address swapFacility_) JMIExtension(mToken_, swapFacility_) { _disableInitializers(); } function initialize( string memory name, string memory symbol, address yieldRecipient_, address admin, address assetCapManager, address freezeManager, address pauser, address yieldRecipientManager ) public override initializer { JMIExtension.initialize( name, // "My JMI USD" symbol, // "jUSD" yieldRecipient_, // Treasury wallet address admin, // Admin multisig address assetCapManager, // Asset cap manager address freezeManager, // Freeze manager address pauser, // Pauser address yieldRecipientManager // Rewards recipient manager address ); } } ``` #### Key Parameters Explained - **`name`**: The full name of your token (e.g., "My JMI USD") - **`symbol`**: The token symbol (e.g., "jUSD") - `yieldRecipient_`: The wallet receiving all rewards (your treasury) - **`admin`**: Address with `DEFAULT_ADMIN_ROLE` (should be a multisig) - **`assetCapManager`**: Address with `ASSET_CAP_MANAGER_ROLE` (manages collateral caps) - **`freezeManager`**: Address with `FREEZE_MANAGER_ROLE` (can freeze addresses) - **`pauser`**: Address with `PAUSER_ROLE` (emergency stop) - **`yieldRecipientManager`**: Address with `YIELD_RECIPIENT_MANAGER_ROLE` **Note:** The `mToken` and `swapFacility` addresses are set in the constructor as immutable variables. ### 3. Write Deployment Script Create a deployment script in the `script/` directory: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Script, console } from "forge-std/Script.sol"; import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { MyJMIToken } from "../src/MyJMIToken.sol"; contract DeployMyJMIToken is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address deployer = vm.addr(deployerPrivateKey); // Configuration - update these addresses for your deployment address mToken = 0x...; // M Token address for your chain address swapFacility = 0x...; // SwapFacility address for your chain vm.startBroadcast(deployerPrivateKey); address proxy = Upgrades.deployTransparentProxy( "MyJMIToken.sol", deployer, abi.encodeCall( MyJMIToken.initialize, ( "My JMI USD", // name "jUSD", // symbol 0x..., // rewards recipient 0x..., // admin 0x..., // assetCapManager 0x..., // freezeManager 0x..., // pauser 0x... // rewards recipient manager ) ), Upgrades.DeployOptions({ constructorData: abi.encode(mToken, swapFacility) }) ); vm.stopBroadcast(); console.log("MyJMIToken deployed at:", proxy); } } ``` ### 4. Testing Your Contract Create comprehensive tests before deployment: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Test } from "forge-std/Test.sol"; import { MyJMIToken } from "../src/MyJMIToken.sol"; import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol"; contract MyJMITokenTest is Test { MyJMIToken token; address admin = makeAddr("admin"); address assetCapManager = makeAddr("assetCapManager"); address treasury = makeAddr("treasury"); address user = makeAddr("user"); address mockUSDC; address mockDAI; function setUp() public { // Deploy mock tokens // Deploy your JMI token // Initialize with test addresses } function testSetAssetCap() public { uint256 usdcCap = 1_000_000e6; // 1M USDC vm.prank(assetCapManager); token.setAssetCap(mockUSDC, usdcCap); assertEq(token.assetCap(mockUSDC), usdcCap); assertTrue(token.isAllowedAsset(mockUSDC)); } function testWrapWithUSDC() public { // Setup: Set cap and mint USDC to swap facility uint256 amount = 1000e6; // Test wrapping USDC through SwapFacility // Assert JMI tokens minted // Assert totalAssets increased } function testUnwrapLimitedToMBacking() public { // Setup: Wrap some M and some USDC // Try to unwrap more than M backing // Expect revert with InsufficientMBacking } function testReplaceAssetWithM() public { // Setup: Have USDC in the contract // Call replaceAssetWithM to swap M for USDC // Assert totalAssets decreased // Assert USDC sent to recipient } function testFreezing() public { vm.prank(admin); // Assuming admin has freeze manager role token.freeze(user); assertTrue(token.isFrozen(user)); } function testYieldClaim() public { // Setup: Wrap M and let rewards accrue // Call claimYield // Assert rewards went to treasury } } ``` ### 5. Configure Supported Collateral After deployment, the asset cap manager must configure which collateral assets are accepted: ```solidity // Set caps for each supported stablecoin // Cap is in the asset's native decimals // USDC (6 decimals) - 10M cap myJMIToken.setAssetCap(USDC_ADDRESS, 10_000_000e6); // DAI (18 decimals) - 5M cap myJMIToken.setAssetCap(DAI_ADDRESS, 5_000_000e18); // USDT (6 decimals) - 5M cap myJMIToken.setAssetCap(USDT_ADDRESS, 5_000_000e6); ``` **Important:** Only set caps for stablecoins that maintain a reliable 1:1 peg to the dollar. The JMI model assumes 1:1 peg for all accepted collateral. ### 6. Gain M0 Earner Approval For your contract to accrue rewards on the supply it holds, its deployed address must be approved as an M0 Earner. - **Calculate Your Address:** Determine your contract's final deployment address using `CREATE2` or by deploying to a testnet first. - **Submit a Proposal:** Create and submit a proposal to add your contract's address to the `earners` list in the `TTGRegistrar`. For more details, see the [Gaining Earner Approval](https://docs.m0.org/protocol/roles) guide. ### 7. Security & Audit Even though you are building on an audited template, any modifications should be thoroughly tested and independently audited: - Write comprehensive tests covering all scenarios - Run static analysis tools like Slither - Engage a reputable security firm for a full audit if you've made significant modifications ### 8. Deploy & Launch #### Deploy to Testnet First ```bash source .env forge script script/DeployMyJMIToken.s.sol \ --rpc-url $SEPOLIA_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast ``` #### Deploy to Mainnet ```bash source .env forge script script/DeployMyJMIToken.s.sol \ --rpc-url $MAINNET_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast ``` #### Hardware Wallet Deployment For production deployments: ```bash forge script script/DeployMyJMIToken.s.sol \ --rpc-url $MAINNET_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --ledger \ --broadcast ``` #### Enable Earning After deployment and earner approval: ```solidity myJMIToken.enableEarning(); ``` ### 9. Integration & Usage #### Managing Asset Caps ```solidity // Check current cap uint256 currentCap = myJMIToken.assetCap(USDC_ADDRESS); // Update cap (only ASSET_CAP_MANAGER_ROLE) myJMIToken.setAssetCap(USDC_ADDRESS, newCap); // Disable an asset (set cap to 0) myJMIToken.setAssetCap(USDC_ADDRESS, 0); ``` #### Checking Backing Status ```solidity // Total JMI supply uint256 totalSupply = myJMIToken.totalSupply(); // Non-M asset backing uint256 totalAssets = myJMIToken.totalAssets(); // M backing (what can be unwrapped) uint256 mBacking = totalSupply - totalAssets; // Specific asset backing uint256 usdcBacking = myJMIToken.assetBalanceOf(USDC_ADDRESS); ``` #### Claiming Rewards ```solidity // Check claimable rewards uint256 pendingYield = myJMIToken.yield(); // Claim rewards (anyone can call, goes to yieldRecipient) uint256 claimedAmount = myJMIToken.claimYield(); ``` #### Managing Rewards Recipient ```solidity // Only YIELD_RECIPIENT_MANAGER_ROLE myJMIToken.setYieldRecipient(newTreasuryAddress); ``` #### Freezing Addresses ```solidity // Only FREEZE_MANAGER_ROLE myJMIToken.freeze(suspiciousAddress); myJMIToken.unfreeze(clearedAddress); // Batch operations address[] memory toFreeze = new address[](3); toFreeze[0] = addr1; toFreeze[1] = addr2; toFreeze[2] = addr3; myJMIToken.freezeAccounts(toFreeze); ``` #### Emergency Pause ```solidity // Only PAUSER_ROLE myJMIToken.pause(); // Halts wrap, unwrap, transfer, replaceAssetWithM myJMIToken.unpause(); // Resumes operations ``` #### Frontend Integration ```javascript // Get backing breakdown const totalSupply = await myJMIToken.totalSupply(); const totalAssets = await myJMIToken.totalAssets(); const mBacking = totalSupply - totalAssets; // Check specific asset backing const usdcBacking = await myJMIToken.assetBalanceOf(USDC_ADDRESS); const daisBacking = await myJMIToken.assetBalanceOf(DAI_ADDRESS); // Check if wrap is allowed const canWrap = await myJMIToken.isAllowedToWrap(USDC_ADDRESS, amount); // Check if unwrap is allowed const canUnwrap = await myJMIToken.isAllowedToUnwrap(amount); // Check if asset replacement is allowed (amount in asset decimals) const canReplace = await myJMIToken.isAllowedToReplaceAssetWithM( USDC_ADDRESS, assetAmount, ); // Get rewards info const pendingYield = await myJMIToken.yield(); const yieldRecipient = await myJMIToken.yieldRecipient(); // Check account status const isFrozen = await myJMIToken.isFrozen(userAddress); const isPaused = await myJMIToken.paused(); ``` ### 10. Monitoring & Maintenance #### Key Metrics to Track | Metric | Description | | ------------------ | ----------------------------------- | | Total Supply | Total JMI tokens in circulation | | M Backing | `totalSupply - totalAssets` | | Total Non-M Assets | Sum of all collateral backings | | Per-Asset Backing | Individual collateral levels | | Asset Utilization | `assetBalance / assetCap` per asset | | Rewards Rate | Current rewards accrual rate | | Pending Rewards | Unclaimed rewards amount | #### Operational Considerations 1. **Monitor Asset Caps:**Regularly review if caps need adjustment based on: - Stablecoin peg stability - Market conditions - Protocol risk tolerance 2. **Rebalancing:** Use `replaceAssetWithM`to rebalance backing when needed: - Arbitrageurs can swap approved collateral for approved stablecoin collateral. - This naturally moves the backing toward approved collateral over time. 3. **Rewards Claiming:** Set up automated rewards claiming if desired. 4. **Emergency Procedures:** - Document pause/unpause procedures. - Have freeze manager ready for suspicious activity. - Maintain secure communication channels for emergencies. 5. **Collateral Monitoring:** - Track peg stability of accepted stablecoins. - Be prepared to reduce/remove caps for depegged assets. **Congratulations!** You now have a fully functional multi-collateral stablecoin that accepts various stablecoins while automatically accumulating rewards for your protocol. # Overview ::note The JMI extension is currently available on EVM chains only (Ethereum, Base, Arbitrum, etc.). SVM support is planned for a future release. :: The `JMIExtension` (Just Mint It) is an enhanced Treasury Model implementation that accepts multiple collateral types while still directing 100% of rewards to a single recipient. **When to Use JMI instead of standard MYieldToOne:** - Projects requiring flexibility to accept multiple stablecoins as collateral types (USDC, USDT, DAI). - Protocols wanting a non-rebasing stablecoin backed by a diversified collateral pool. - Ecosystems that need to manage risk exposure to different stablecoin assets through configurable caps. - Treasury management scenarios where rewards are centralized but collateral sources are diversified. **Source Code:** [`JMIExtension.sol`](https://github.com/m0-foundation/m-extensions/blob/main/src/projects/jmi/JMIExtension.sol){rel=""nofollow""} ## Key Differentiators from MYieldToOne | Feature | MYieldToOne (Standard) | JMIExtension (JMI) | | --------------------- | ---------------------- | ----------------------------------------------------------------------- | | Collateral | Single-collateral | Multi-collateral (liquid stablecoins such as USDC, USDT also supported) | | Asset Caps | N/A | Configurable per asset | | Unwrap Logic | Always backed 1:1 | Limited to base backing portion | | Additional Operations | None | `replaceAssetWithM` | ## Architecture Overview `JMIExtension` inherits from `MYieldToOne`, which itself inherits from `MExtension`, `Freezable`, and `Pausable`. This inheritance chain provides: - **ERC-20 Token Logic** via `MExtension` - **Rewards Distribution** via `MYieldToOne` (100% to single recipient) - **Compliance Controls** via `Freezable` (address-level restrictions) - **Emergency Controls** via `Pausable` (contract-wide halt) - **Multi-Collateral Logic** via `JMIExtension` (asset caps, multiple collateral types) ```text JMIExtension ├── IJMIExtension (interface) ├── JMIExtensionLayout (storage) └── MYieldToOne ├── IMYieldToOne (interface) ├── MYieldToOneStorageLayout (storage) ├── MExtension (core ERC-20 + wrap/unwrap) ├── Freezable (address freezing) └── Pausable (emergency stop) ``` ## How It Works 1. **Multi-Collateral Wrapping:** Users can wrap an approved stablecoin (USDC, DAI, etc.) through the SwapFacility. The contract tracks each asset's balance separately. 2. **Asset Caps:** Each stablecoin collateral component has a maximum cap. This limits risk exposure to any single stablecoin. 3. **Rewards Accrual:** Only the non-stablecoin collateral portion of the reserves earns rewards. 4. **Rewards Distribution:** The `claimYield()` function mints new JMI tokens to the designated `yieldRecipient`, equal to the rewards surplus. 5. **Selective Unwrapping:** Users can only unwrap to the base asset. The maximum unwrap amount is limited to the non-stablecoin collateral portion of reserves (total supply minus stablecoin asset backing). 6. **Asset Replacement:** The `replaceAssetWithM` function allows swapping approved collateral for approved stablecoins held by the contract, enabling arbitrageurs to rebalance the backing. ## Backing Model The total supply of JMI tokens is backed by two components: ```text Total Supply = M Backing + Total Non-M Assets ``` Where: - **M Backing** = `totalSupply() - totalAssets()` - **Total Non-M Assets** = Sum of all approved stablecoin collateral (in extension decimals) The claimable rewards are calculated as: ```text Rewards = mBalanceOf(contract) - M Backing ``` The JMI backing model assumes a 1:1 peg between all deposited assets. This means 1 USDC deposited equals 1 JMI token minted, and 1 DAI deposited equals 1 JMI token minted (accounting for decimal conversions). ## Roles (Access Control) JMIExtension uses OpenZeppelin's `AccessControl` with these roles: | Role | Permissions | | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | **`DEFAULT_ADMIN_ROLE`** | Super-user. Can grant and revoke any role. Should be a secure multi-sig or governance contract. | | **`ASSET_CAP_MANAGER_ROLE`** | Manages risk by setting caps on each approved stablecoin collateral asset via `setAssetCap()`. | | **`YIELD_RECIPIENT_MANAGER_ROLE`** | Controls where rewards are distributed via `setYieldRecipient()`. | | **`FREEZE_MANAGER_ROLE`** | Can freeze/unfreeze addresses from interacting with the token. | | **`PAUSER_ROLE`** | Can pause/unpause the entire contract in emergencies. | ## Core Functions ### Wrapping Functions | Function | Description | | -------------------------------- | -------------------------------------------------------------------------------------------------------- | | `wrap(asset, recipient, amount)` | Mints JMI tokens by depositing an approved stablecoin collateral asset. Called via SwapFacility. | | `wrap(recipient, amount)` | Mints JMI tokens by depositing approved collateral (inherited from MExtension). Called via SwapFacility. | ### Unwrapping Functions | Function | Description | | --------------------------- | ---------------------------------------------------------------- | | `unwrap(recipient, amount)` | Burns JMI tokens and sends approved collateral to the recipient. | ### Asset Management Functions | Function | Access | Description | | --------------------------------------------- | ------------------------ | ------------------------------------------------------------------------ | | `setAssetCap(asset, cap)` | `ASSET_CAP_MANAGER_ROLE` | Sets the maximum balance allowed for a collateral asset. | | `replaceAssetWithM(asset, recipient, amount)` | SwapFacility only | Swaps approved collateral for approved stablecoins held by the contract. | ### Rewards Functions (Inherited) | Function | Description | | ---------------------------- | ---------------------------------------------------------- | | `yield()` | Returns the current claimable rewards amount. | | `claimYield()` | Claims rewards by minting new JMI tokens to the recipient. | | `setYieldRecipient(address)` | Updates the rewards recipient address. | ### View Functions | Function | Returns | Description | | --------------------------------------------- | --------- | --------------------------------------------------------- | | `assetBalanceOf(asset)` | `uint256` | Tracked balance of a collateral asset | | `assetCap(asset)` | `uint256` | Cap for a collateral asset (0 = not allowed) | | `assetDecimals(asset)` | `uint8` | Cached decimals for a collateral asset | | `totalAssets()` | `uint256` | Total approved stablecoin backing (in extension decimals) | | `isAllowedAsset(asset)` | `bool` | True if asset is approved collateral or has non-zero cap | | `isAllowedToWrap(asset, amount)` | `bool` | True if wrap would succeed | | `isAllowedToUnwrap(amount)` | `bool` | True if unwrap would succeed | | `isAllowedToReplaceAssetWithM(asset, amount)` | `bool` | True if asset balance >= amount | ### Freezing & Pause Functions (Inherited) | Function | Access | Description | | ------------------------------ | --------------------- | --------------------------- | | `freeze(account)` | `FREEZE_MANAGER_ROLE` | Freezes an address | | `unfreeze(account)` | `FREEZE_MANAGER_ROLE` | Unfreezes an address | | `freezeAccounts(accounts[])` | `FREEZE_MANAGER_ROLE` | Batch freeze | | `unfreezeAccounts(accounts[])` | `FREEZE_MANAGER_ROLE` | Batch unfreeze | | `isFrozen(account)` | Public | Check if address is frozen | | `pause()` | `PAUSER_ROLE` | Pauses the contract | | `unpause()` | `PAUSER_ROLE` | Unpauses the contract | | `paused()` | Public | Check if contract is paused | ## Events | Event | Parameters | Description | | -------------------- | ---------------------------------------------- | ------------------------- | | `AssetCapSet` | `asset`, `cap` | Asset cap was updated | | `AssetReplacedWithM` | `asset`, `assetAmount`, `recipient`, `mAmount` | Collateral swapped | | `YieldClaimed` | `amount` | Rewards claimed | | `YieldRecipientSet` | `yieldRecipient` | Rewards recipient changed | | `Transfer` | `from`, `to`, `amount` | ERC-20 transfer | | `Approval` | `owner`, `spender`, `amount` | ERC-20 approval | | `Frozen` | `account`, `timestamp` | Account was frozen | | `Unfrozen` | `account`, `timestamp` | Account was unfrozen | | `Paused` | `account` | Contract was paused | | `Unpaused` | `account` | Contract was unpaused | ## Errors | Error | Description | | ------------------------------------------------------ | --------------------------------------------------------- | | `InvalidAsset(asset)` | Asset is `address(0)` or `$M` (for multi-asset functions) | | `AssetCapReached(asset)` | Deposit would exceed asset cap | | `InsufficientAssetBacking(asset, required, available)` | Not enough collateral for replacement | | `InsufficientAssetReceived(asset, expected, received)` | Fee-on-transfer token detected | | `InsufficientMBacking(required, available)` | Not enough approved collateral backing for unwrap | | `ZeroAssetCapManager()` | Asset cap manager is zero address | | `ZeroAdmin()` | Admin is zero address | | `ZeroYieldRecipient()` | Rewards recipient is zero address | | `ZeroYieldRecipientManager()` | Rewards recipient manager is zero address | | `ZeroFreezeManager()` | Freeze manager is zero address | | `ZeroPauser()` | Pauser is zero address | | `AccountFrozen(account)` | Operation blocked due to frozen account | | `EnforcedPause()` | Operation blocked due to paused state | ## Important Constraints - **Fee-on-Transfer Tokens:** Not supported. The contract reverts if fewer tokens are received than expected. - **Only 1:1 Pegged Stablecoins:** The model assumes all accepted collateral is pegged 1:1 to the dollar. - **Unwrap Limited to Approved Collateral Backing:** Users cannot unwrap more JMI than the contract's approved collateral backing supports. ## Security Considerations 1. **Fee-on-Transfer Protection:** The contract explicitly checks received amounts match expected amounts, reverting on discrepancies. 2. **Inflation Attack Prevention:** The `replaceAssetWithM` function uses the tracked `assetBalanceOf` rather than actual token balance, preventing manipulation via direct token transfers. 3. **Access Control:** All sensitive functions are protected by role-based access control. 4. **Emergency Controls:** The `PAUSER_ROLE` can halt operations in case of security incidents. 5. **Frozen Accounts:** The `FREEZE_MANAGER_ROLE` can block compromised addresses from interacting with the token. 6. **Upgradability:** The contract uses OpenZeppelin's transparent proxy pattern, allowing for future upgrades with proper governance. **Ready to build?** [**Follow the JMI implementation guide**](https://docs.m0.org/build/treasury-jmi-guide) # Overview **Core Concept:** All accrued rewards are captured by the use case owner. Token holders observe a stable, non-rebasing token with 1:1 backing. **When to Use:** - Simple business models where all rewards go to one entity. - Corporate treasuries requiring centralized revenue management. - Protocol-owned stablecoins where rewards fund protocol development. - Ecosystem development funds or grants programs. ## EVM Implementation Options The Treasury Model offers two EVM implementations to fit different collateral strategies: | Feature | MYieldToOne (Standard) | JMIExtension (Multi-Collateral) | | ------------------------- | ---------------------- | ----------------------------------------------------------------------- | | **Collateral** | Single-collateral | Multi-collateral (liquid stablecoins such as USDC, USDT also supported) | | **Asset Caps** | N/A | Configurable per asset | | **Unwrap Logic** | Always 1:1 backed | Limited to base backing portion | | **Additional Operations** | None | `replaceAssetWithM` for rebalancing | | **Best For** | Simple treasury setups | Diversified collateral pools | **Choose `MYieldToOne`** for single-collateral deployments with the simplest implementation. **Choose** `JMIExtension` if you need flexibility to accept multiple stablecoins while still directing all rewards to a single recipient. [**Learn more about JMI**](https://docs.m0.org/build/treasury-jmi-overview) ## Default: MYieldToOne The `MYieldToOne` template is the simplest and most direct way to build on M0. It creates an upgradeable ERC-20 token backed 1:1 by eligible collateral, while directing **100% of the accrued rewards** to a single, designated recipient address. This model is perfect for builders who want the stability and liquidity of an M0-powered token within their ecosystem, but prefer to centralize the rewards for strategic purposes like funding operational expenses, protocol development, ecosystem grants. **Source Code:** [`MYieldToOne.sol`](https://github.com/m0-foundation/m-extensions/blob/main/src/projects/yieldToOne/MYieldToOne.sol){rel=""nofollow""} ### Architecture and Mechanism The beauty of `MYieldToOne` lies in its simplicity. It separates the token holding experience from the rewards generation, while incorporating freezing capabilities for compliance. #### Roles (Access Control) `MYieldToOne` inherits from both `MExtension` and `Freezable`, using OpenZeppelin's `AccessControl` to manage permissions with these distinct roles: - **`DEFAULT_ADMIN_ROLE`**: The super-user. This role can grant and revoke any other role. It should be held by a secure multi-sig or governance contract. - `YIELD_RECIPIENT_MANAGER_ROLE`: This role has one job: to manage where the rewards go. It can call `setYieldRecipient()` to change the beneficiary address. - **`FREEZE_MANAGER_ROLE`**: This role can block specific addresses from wrapping, unwrapping, or transferring the token, adding a layer of compliance through the `freeze()` and `unfreeze()` functions. #### How It Works 1. **Wrapping:** A user wraps through the SwapFacility and receives an equal amount of your new extension token (e.g., wrap 100, receive 100 `YourToken`). The `MYieldToOne` contract now holds the underlying balance. 2. **Accruing Rewards:** Your deployed `MYieldToOne` contract must call `enableEarning()` to start earning rewards. It begins to accrue rewards from the underlying eligible collateral. 3. **Creating a Surplus:** As rewards accrue, the contract's balance becomes greater than the total supply of your extension token. For example, the contract might hold 100.05 while the `totalSupply()` of `YourToken` is still 100. 4. **Claiming Rewards:** This surplus is the claimable rewards. Anyone can call the `claimYield()` function to realize this revenue. 5. **Distribution:** When `claimYield()` is called, the contract mints new `YourToken` equivalent to the surplus amount (0.05 in our example) and sends them directly to the designated `yieldRecipient` address. The `totalSupply` now becomes 100.05. For end-users holding your token, their balance remains stable and unchanged. The rewards are handled completely behind the scenes. ### Key Interface & Functions While `MYieldToOne` is a full ERC-20 token, its unique logic revolves around a few key functions. #### Storage Layout The state is kept minimal and efficient. ```solidity struct MYieldToOneStorageStruct { uint256 totalSupply; address yieldRecipient; mapping(address account => uint256 balance) balanceOf; } ``` #### Core Rewards Functions - `yield() external view returns (uint256)` This view function calculates the amount of claimable rewards at any moment. It returns the difference between the contract's current balance and the `totalSupply` of the extension token: `mBalance > totalSupply ? mBalance - totalSupply : 0`. - `claimYield() external returns (uint256)` This is the function that triggers the rewards distribution. It calculates the rewards, mints that amount of new tokens, and transfers them to the `yieldRecipient`. Returns 0 if there are no rewards to claim. #### Management Functions - **`setYieldRecipient(address yieldRecipient) external`** Callable only by the `YIELD_RECIPIENT_MANAGER_ROLE`, this function updates the address that receives the rewards. It automatically claims any existing rewards for the previous recipient before making the change. #### Freezing Functions (Inherited from Freezable) - **`freeze(address account) external`** Callable by the `FREEZE_MANAGER_ROLE`, this function prevents an address from interacting with the token. Reverts if the account is already frozen. - **`unfreeze(address account) external`** Removes an address from the frozen list. Reverts if the account is not currently frozen. - **`isFrozen(address account) external view returns (bool)`** Checks if an address is currently frozen. - **`freezeAccounts(address[] calldata accounts) external`** Batch version for freezing multiple accounts. - **`unfreezeAccounts(address[] calldata accounts) external`** Batch version for unfreezing multiple accounts. #### Standard MExtension Functions As a child of `MExtension.sol`, `MYieldToOne` automatically inherits all the essential functionalities for interacting with the M0 ecosystem: - `wrap(address recipient, uint256 amount)` - Called by SwapFacility only. - `unwrap(address recipient, uint256 amount)` - Called by SwapFacility only. - `enableEarning()` - Starts earning rewards. - `disableEarning()` - Stops earning rewards. #### Hook Functions The contract implements several internal hook functions that enforce freezing: - `_beforeApprove()` - Checks both account and spender aren't frozen. - `_beforeWrap()` - Checks both depositor and recipient aren't frozen. - `_beforeUnwrap()` - Checks the account isn't frozen. - `_beforeTransfer()` - Checks sender, recipient, and msg.sender aren't frozen. #### Constants - **`YIELD_RECIPIENT_MANAGER_ROLE`**: `keccak256("YIELD_RECIPIENT_MANAGER_ROLE")` - **`FREEZE_MANAGER_ROLE`**: `keccak256("FREEZE_MANAGER_ROLE")` (inherited from Freezable) **Ready to build with MYieldToOne?** [**Follow the implementation guide**](https://docs.m0.org/build/treasury-guide) **Need multi-collateral support?** [**See the JMI implementation guide**](https://docs.m0.org/build/treasury-jmi-guide) ## SVM Implementation: NoYield The `NoYield` extension template is the most direct way to build a treasury-focused stablecoin on Solana. It creates an upgradeable, non-rebasing token backed 1:1 by eligible collateral, while directing **100% of the accrued rewards** to the extension's administrator. The NoYield variant supports both the Token-2022 standard and the legacy token program. This model is perfect for builders who want the stability of an M0-powered token within their ecosystem but prefer to centralize rewards for strategic purposes, such as funding operational expenses, protocol development, ecosystem grants. **Source Code:** The `NoYield` model is compiled from the `m_ext` program using the `no-yield` Rust feature flag. The source code is available in the [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} repository. ### Architecture and Mechanism The `NoYield` model's strength is its simplicity. It completely separates token holding from rewards generation, which is managed centrally by the platform. #### Roles (Access Control) - **`admin`**: The root administrator, typically a secure multi-sig or governance contract. This role can: - Configure the extension via `initialize`. - Claim all accrued rewards using the `claim_fees` instruction. - Manage a whitelist of `wrap_authorities`. - **`wrap_authority`**: An address authorized by the `admin` to perform wrap and unwrap operations. This is useful for delegating wrapping capabilities to specific programs or services, like the swap facility. #### How It Works 1. **Wrapping:** A user or program with `wrap_authority` wraps and receives an equal amount of the new extension token. The `NoYield` contract's vault (`m_vault`) now holds the underlying balance. 2. **Rewards Accrual:** The contract's vault is registered as an earner in the base program. It begins to accrue rewards through Solana's rebasing mechanism. 3. **Creating a Surplus:** As rewards accrue, the contract's balance in its vault becomes greater than the total supply of the extension token. For example, the contract might hold 100.05 while the `totalSupply` of the extension token remains 100. This surplus represents the claimable rewards. 4. **Claiming Rewards:** The `admin` calls the `claim_fees` function. This is the core mechanism for capturing the protocol's revenue. 5. **Distribution:** The `claim_fees` instruction calculates the surplus, mints an equivalent amount of new extension tokens, and sends them directly to a recipient token account controlled by the `admin`. The `totalSupply` of the extension token is now updated to reflect the newly minted fee tokens. For end-users, their token balance remains stable and unchanged. The rewards are handled entirely behind the scenes by the protocol's administrator. ### Key Interface & Functions The `NoYield` model is a feature-complete Token-2022 token. Its unique logic is centered around a few key instructions defined in the generic Extension program. #### Storage Layout The extension's state is managed by the `ExtGlobal` account, which is a PDA seeded with `b"ext_global"`. ```rust // State structure from m_ext program pub struct ExtGlobal { pub admin: Pubkey, pub ext_mint: Pubkey, pub m_mint: Pubkey, pub m_earn_global_account: Pubkey, pub bump: u8, pub m_vault_bump: u8, pub ext_mint_authority_bump: u8, pub yield_config: YieldConfig, pub wrap_authorities: Vec, } pub struct YieldConfig { pub variant: YieldVariant, // Set to YieldVariant::NoYield pub last_m_index: u64, pub last_ext_index: u64, } ``` #### Core Rewards Function - `claim_fees()`: The primary instruction for capturing rewards. It calculates the total rewards accrued in the `m_vault` since the last claim, mints that amount of new extension tokens, and transfers them to an admin-controlled `recipient_ext_token_account`. This instruction can only be called by the `admin`. #### Management Functions - **`initialize(wrap_authorities: Vec)`**: Sets up the extension's global state with the NoYield configuration. - **`add_wrap_authority(new_wrap_authority: Pubkey)`**: Adds a new address to the list of authorized wrappers. - **`remove_wrap_authority(wrap_authority: Pubkey)`**: Removes an address from the list of authorized wrappers. - **`transfer_admin(new_admin: Pubkey)`**: Transfers admin control to a new address. #### Standard Extension Functions The contract inherits essential functionalities for interacting with the M0 ecosystem: - **`wrap(amount: u64)`**: Wraps into the extension token. Requires the caller to be a whitelisted `wrap_authority`. - **`unwrap(amount: u64)`**: Unwraps the extension token. Requires the caller to be a whitelisted `wrap_authority`. #### Error Handling The contract includes robust error handling to ensure secure operation: - `Unauthorized`: Thrown if a non-admin tries to call a privileged instruction. - `InvalidAmount`: Thrown for invalid wrap or unwrap amounts. - `InsufficientCollateral`: Ensures the contract never mints more extension tokens than its underlying balance. **Ready to build?** [**Follow the implementation guide to deploy your NoYield extension.**](https://docs.m0.org/build/treasury-guide) # Overview Stablecoin issuance is becoming a strategic line of business for many financial institutions. The question is no longer whether to participate - it's whether you build the infrastructure yourself or use the best stack available. M0's answer is **Stablecoin Core**. --- ## What it means to be an issuer powered by M0 As an issuer utilizing the M0 infrastructure, you pledge eligible collateral - specifically, permissible investments in line with local regulations - and mint application-specific stablecoins. As an issuer, you can either utilize the M0 stack to issue your own stablecoin, or optionally partner with builders who want their own application-specific stablecoins. You manage the reserve side of the business. M0 gives you the tech to issue and distribute digital money on-chain. --- ## Why issue using M0 technology **You get the best issuance tech stack in the market.** Mint and distribute any number of stablecoins across most major blockchains. Configure partner-specific, real-time rewards distribution programs. Monitor all data associated with issued stablecoins and reconcile with off-chain ledgers. **You go live fast.** Start issuing in a matter of weeks - not the 18-month build cycle that proprietary infrastructure typically requires. **You reach the right builders.** PayPal, MetaMask, Exodus, Kast, and others already demand M0-standard stablecoins. When you become an M0 issuer, you get immediate access to builders who are actively looking for issuance partners. **You earn from the relationship.** Real-time rewards distribution means your clients get near-instant rewards from the collateral backing their stablecoins. You manage the program parameters. You control the revenue sharing logic. **You maintain optionality.** M0 Stablecoin Core is designed to handle multiple clients on multiple chains from a single deployment. You're not locked into one product or one market. --- ## The product: Stablecoin Core Stablecoin Core is M0's top-level issuance product. It's the software stack that powers your issuance operation. What's included: - Mint and burn mechanics across supported chains - Configurable real-time rewards distribution - Onchain data monitoring and off-chain reconciliation tools - Partner-level revenue sharing program management - Multi-currency support (expand to new currencies without rebuilding) A Stablecoin Core is specific to an issuer and to a currency. You can deploy multiple Stablecoin Cores - one per currency you want to issue in. You control all parameters of each Core. --- ## Optional: Onchain Orchestration Once you're issuing, you may want to distribute your stablecoin into your applications, or you may want to help your builder clients distribute their stablecoin into real use cases. M0's Onchain Orchestration service handles this. With Onchain Orchestration, you can: - Swap USDC for your issued stablecoin (and vice versa) - Handle intra-issuer and cross-issuer stablecoin conversions - Move stablecoins cross-chain - Deliver liquidity to builder clients without building custom plumbing This is available as an additional license on top of Stablecoin Core. [Onchain Orchestration →](https://docs.m0.org/build/accessing-liquidity) --- ## Who this is for M0 is built for institutions that already operate - or are ready to operate - in the digital asset space with the appropriate licenses and charters: - **SIaaS providers** (Stablecoin Issuance as a Service) - companies like Bridge (Stripe), MoonPay, and Anchorage who want to offer issuance to their clients at scale. - **Crypto-native infrastructure providers** - companies with adjacent licenses (rails, custody) expanding into issuance. - **Vertically integrated institutions** - banks, fintechs, and financial networks that want to issue a stablecoin for their own distribution. --- ## How to get started 1. **Talk to us.** Every issuer relationship starts with a conversation. We'll assess fit, walk through the technical requirements, and outline the onboarding process for your specific situation. 2. **Complete the technical onboarding.** Once aligned, the M0 team will guide you through Stablecoin Core setup and integration with your backoffice systems. 3. **Go live.** Start issuing stablecoins to your builder clients. [Get in Touch →](https://www.m0.org/contact-us){rel=""nofollow""} --- ## Related ::card-group :::card --- icon: i-lucide-network title: Protocol overview to: https://docs.m0.org/protocol/m-token --- Understand the technical architecture behind M0. ::: :::card --- icon: i-lucide-refresh-cw title: Minting & Burning to: https://docs.m0.org/protocol/minting-burning --- How the MinterGateway manages supply creation and redemption. ::: :::card --- icon: i-lucide-users title: Roles to: https://docs.m0.org/protocol/roles --- Understand Minters, Validators, and Earners in the protocol. ::: :: # Distribution Vault Also called `TTGVault` or `Vault`. ## Overview The Distribution Vault is a core component of M0's economic infrastructure designed to collect, account for, and distribute various forms of protocol revenue and yield to Zero token holders. It serves as the primary mechanism for value capture within the protocol's Two Token Governance (TTG) system, turning protocol operations into direct economic benefits for governance participants. ## Purpose and Role in the Ecosystem The Distribution Vault functions as: 1. **Revenue Collector**: Accumulates various forms of protocol revenue from multiple sources 2. **Yield Distributor**: Enables proportional distribution of accumulated tokens to Zero token holders 3. **Value Capture Mechanism**: Creates a direct incentive for governance participation 4. **Economic Bridge**: Links protocol activity with governance token value ## Architecture Overview The Distribution Vault is referenced throughout the system as `TTGVault` or simply `vault`. Its address is established during deployment and made accessible to other core contracts via the `Registrar` or specific deployer contracts. ![Adopted Guidance Change Process Flow](https://docs.m0.org/images/technical-documentations/distribution-vault/distribution-flow.png) ## Revenue Sources The Distribution Vault accumulates tokens from four primary sources: ### 1. Excess Yield (from `MinterGateway`) This represents a continuous source of revenue coming from the interest rate spread between minters and earners. **Mechanism:** - The `MinterGateway.updateIndex()` function calculates `excessOwedM()` - the difference between total owed `$M` (based on minter interest rate) and actual total `$M` supply (growing at the earner interest rate) - When positive, this excess amount is minted directly to the Distribution Vault **Origin of Excess:** - **Interest Rate Spread**: The inherent difference between the higher interest rate charged to minters vs. the lower rate paid to earners (capped at 98% of the "safe rate") - **Rounding Differences**: Accumulated from principal-to-present value conversions within the continuous indexing math - **Penalties**: Charges imposed on minters for missed collateral updates or undercollateralization **Code Implementation:** ```solidity // In MinterGateway.sol function updateIndex() public override(IContinuousIndexing, ContinuousIndexing) returns (uint128 index_) { uint240 excessOwedM_ = excessOwedM(); if (excessOwedM_ > 0) IMToken(mToken).mint(ttgVault, excessOwedM_); // Mint $M to TTG Vault index_ = super.updateIndex(); // Update minter index and rate IMToken(mToken).updateIndex(); // Update earning index and rate } ``` ### 2. Power Token Auction Proceeds (from Power Token) The Power token contract auctions off newly inflated tokens in non-voting epochs, with proceeds directed to the vault. **Mechanism:** - Power token employs a Dutch auction mechanism where price decreases over time - When users purchase Power token via the `buy()` function, the calculated cost in `cashToken` is sent to the Distribution Vault **Code Implementation:** ```solidity // In Power token.sol function buy( uint256 minAmount_, uint256 maxAmount_, address destination_, uint16 expiryEpoch_ ) external returns (uint240 amount_, uint256 cost_) { // ... calculations for amount_ and cost_ ... emit Buy(msg.sender, amount_, cost_ = getCost(amount_)); _mint(destination_, amount_); if (!ERC20Helper.transferFrom(cashToken(), msg.sender, vault, cost_)) revert TransferFromFailed(); } ``` ### 3. Forfeited Standard Governor Proposal Fees (from `StandardGovernor`) Fees submitted with proposals in the `StandardGovernor` flow to the Distribution Vault if the proposal fails. **Mechanism:** - When creating a proposal via `StandardGovernor.propose()`, proposers pay a fee in the system's `cashToken` - If the proposal succeeds, the fee is returned to the proposer - If the proposal is defeated or expires, the fee is sent to the Distribution Vault via `sendProposalFeeToVault()` **Code Implementation:** ```solidity // In StandardGovernor.sol function sendProposalFeeToVault(uint256 proposalId_) external { ProposalState state_ = state(proposalId_); if (state_ != ProposalState.Expired && state_ != ProposalState.Defeated) revert FeeNotDestinedForVault(state_); uint256 proposalFee_ = _proposalFees[proposalId_].fee; if (proposalFee_ == 0) revert NoFeeToSend(); address cashToken_ = _proposalFees[proposalId_].cashToken; delete _proposalFees[proposalId_]; emit ProposalFeeSentToVault(proposalId_, cashToken_, proposalFee_); _transfer(cashToken_, vault, proposalFee_); } ``` ### 4. Direct/Arbitrary Transfers The Distribution Vault can receive tokens via direct transfers from any account or contract. These tokens need to be explicitly "registered" for distribution. **Mechanism:** - Tokens can be transferred directly to the vault contract address at any time using a standard ERC20 `transfer` or `transferFrom`. - To make these externally transferred tokens available for claiming, the external `distribute(token)` function must be called for the specific `token_` address. - The `distribute(token)` function calculates the increase in the vault's balance of `token_` since the last time distribute was called for that token (using the internal `_lastTokenBalances` tracking). It then registers this newly available amount (`amount_`) within the `currentEpoch_` (the epoch when `distribute()` is called). - Crucially, Zero token holders who held tokens *during* this `currentEpoch_` become eligible to claim a share of this distributed `amount_`. The claimable share for each eligible holder is calculated pro-rata based on their fractional Zero token ownership as snapshotted at the *end* of that specific `currentEpoch_`. **Code Implementation:** ```solidity // In DistributionVault.sol function distribute(address token_) external returns (uint256 amount_) { uint256 currentEpoch_ = clock(); amount_ = getDistributable(token_); emit Distribution(token_, currentEpoch_, amount_); unchecked { distributionOfAt[token_][currentEpoch_] += amount_; // Add to the distribution for the current epoch _lastTokenBalances[token_] += amount_; // Track this contract's latest balance } } function getDistributable(address token_) public view returns (uint256) { return IERC20(token_).balanceOf(address(this)) - _lastTokenBalances[token_]; } ``` ## Distribution Mechanism The Distribution Vault implements a sophisticated epoch-based, pro-rata distribution system that ensures fair allocation of accumulated tokens to Zero token holders. ### Core Mechanics 1. **Epoch-Based Accounting**: Each token distribution is recorded in the current epoch (a time period, typically 15 days) 2. **Historical Balances**: Claims are calculated based on Zero token holders' balances during past epochs 3. **Pro-Rata Distribution**: Tokens are distributed proportionally to Zero token holdings 4. **Claimable Design**: Zero token holders must claim their share of distributed tokens ### Distribution Process 1. **Accumulation**: Tokens flow into the vault through various mechanisms 2. **Registration**: The `distribute(token)` function is called, recording the new token amount for the current epoch 3. **Claiming**: Zero token holders call `claim()` or `claimBySig()` to receive their share for specific epochs 4. **Calculation**: The claimable amount is computed based on the holder's proportional ownership during each claimed epoch ### Key Functions ```solidity // Allows claiming tokens for a specific address function claim( address token_, uint256 startEpoch_, uint256 endEpoch_, address destination_ ) external returns (uint256 claimed_) // Allows claiming with a signature (helpful for gas-less transactions) function claimBySig( address account_, address token_, uint256 startEpoch_, uint256 endEpoch_, address destination_, uint256 deadline_, bytes memory signature_ ) external returns (uint256 claimed_) // Calculates the claimable amount for a given address, token, and epoch range function getClaimable( address token_, address account_, uint256 startEpoch_, uint256 endEpoch_ ) public view returns (uint256 claimable_) ``` ## Technical Implementation Details ### Contract Structure The Distribution Vault inherits from StatefulERC712, which provides EIP-712 signature verification functionality: ```solidity contract DistributionVault is IDistributionVault, StatefulERC712 { // ... implementation ... } ``` ### Key State Variables ```solidity // The scale to apply when accumulating an account's claimable token to minimize precision loss uint256 internal constant _GRANULARITY = 1e9; // EIP-712 typehash for claim function bytes32 public constant CLAIM_TYPEHASH = 0x4b4633c3c305de33d5d9cf70f2712f26961648cd68d020c2556a9e43be58051d; // Address of the Zero Token contract address public immutable zeroToken; // Mapping of last recorded balance per token mapping(address token => uint256 balance) internal _lastTokenBalances; // Mapping of distributions per token, epoch, and amount mapping(address token => mapping(uint256 epoch => uint256 amount)) public distributionOfAt; // Mapping of claimed status per token, epoch, and account mapping(address token => mapping(uint256 epoch => mapping(address account => bool claimed))) public hasClaimed; ``` ### Signature-Based Claims The Distribution Vault supports signature-based claims, allowing users to authorize others to claim on their behalf: ```solidity function claimBySig( address account_, address token_, uint256 startEpoch_, uint256 endEpoch_, address destination_, uint256 deadline_, uint8 v_, bytes32 r_, bytes32 s_ ) external returns (uint256) { // ... signature verification logic ... // ... deadline verification ... return _claim(account_, token_, startEpoch_, endEpoch_, destination_); } ``` ## Economic Implications The Distribution Vault creates several important economic effects: 1. **Zero Token Value Accrual**: By directing protocol revenue to Zero token holders, it increases the token's economic value 2. **Governance Incentive Alignment**: Creates a direct financial incentive for governance participation 3. **Sustainable Protocol Economics**: Ensures that excess yield and protocol fees flow back to governance participants 4. **Diversified Revenue Streams**: Collects different token types from multiple sources, creating a robust revenue model ## Integration with the Wrapped `$M` Token (Extended) The Distribution Vault also interacts with the Wrapped `$M` (wM) system: 1. **Excess Yield Destination**: The wM contract can send excess yield to the Distribution Vault via its `claimExcess()` function 2. **Economic Loop Completion**: This creates a complete economic loop where even wrapped token operations can generate value for governance participants ## Conclusion The Distribution Vault stands as a critical infrastructure component in the M0 protocol, serving as the central hub for value capture and redistribution. Through its sophisticated pro-rata distribution mechanism, it ensures that protocol operations consistently generate value for governance participants, creating strong economic incentives for system participation and alignment. By collecting revenue from multiple sources and making it claimable by Zero token holders, the vault creates a direct financial link between protocol usage and governance value, strengthening the economic sustainability of the entire ecosystem. One can check straight on the [dashboard](http://dashboard.m0.org){rel=""nofollow""} the number of `$M` accumulated by the vault and not claimed so far. # M0 Extensions ## What Are M0 Extensions? M0 Extensions are the **application layer** of the M0 Protocol. They are custom ERC-20 stablecoins that developers build on the M0 platform, inheriting its security and yield properties while adding their own unique features, branding, and business logic. Think of M0 as the secure, collateralized foundation -- like digital infrastructure. Extensions are the applications, products, and user experiences built on top of this foundation. ### The Extension Architecture ```text Your Custom Stablecoin (Extension) | wraps/unwraps M0 (Foundation) | backed by US Treasury Collateral ``` **Key Relationship:** Extensions are backed 1:1 by eligible collateral, meaning: - 1 Extension Token = 1 unit of value (always redeemable) - Extensions inherit the M0 platform's stability and regulatory clarity - Extension contracts can earn yield (if approved by governance) - Developers control how that yield flows to users, treasuries, or other destinations ### Why Build an Extension? #### Full Customization - **Yield Distribution**: Route yield to users, treasury, or split however you want - **Access Controls**: Implement KYC, geographic restrictions, or institutional-only access - **Fee Mechanisms**: Add transaction fees, management fees, or revenue sharing - **Branding**: Create a fully branded stablecoin (YourAppUSD) or keep it generic #### Inherited Benefits - **Regulatory Clarity**: Built on M0's compliant, audited infrastructure - **Shared Liquidity**: Tap into the M0 ecosystem's shared liquidity layer - **Cross-Chain Ready**: Deploy on any chain where M0 operates - **Battle-Tested Security**: Leverage M0's proven collateral and governance systems #### Business Value - **Control**: Unlike integrating someone else's stablecoin, you own the user relationship - **Revenue**: Capture value through yield management and optional fees - **Flexibility**: Evolve your stablecoin's features as your needs change ### Common Extension Types #### Treasury Extensions Route 100% of yield to a single treasury address while providing users with a stable, non-rebasing token experience. **Use Cases**: Protocol treasuries, ecosystem development funds, corporate reserves #### User Yield Extensions Share yield with token holders while taking a small protocol fee for sustainability. **Use Cases**: DeFi protocols, consumer apps, yield savings accounts #### Institutional Extensions Provide granular, per-account control with custom fee arrangements and whitelisting. **Use Cases**: Prime brokerages, fintech platforms, institutional treasury management ### Who Should Build Extensions? **Recommended for:** - **Application Developers**: Games, payments, DeFi protocols wanting branded stablecoins - **Ecosystem Builders**: L1/L2 chains needing native stablecoins for their ecosystems - **Fintech Companies**: Businesses requiring custom compliance or yield distribution - **Treasury Managers**: Organizations needing yield-bearing accounts with specific controls **Consider Wrapped `$M` instead if:** - You just need standard DeFi integration (AMM pools, lending markets) - You prefer using an existing, established token rather than deploying your own ### Real-World Examples - [**Wrapped `$M`**](https://docs.m0.org/protocol/wrapped-m): The "reference implementation" showing standard DeFi compatibility - [**Noble USD**](https://dollar.noble.xyz/){rel=""nofollow""}: Cross-chain stablecoin serving the Cosmos ecosystem ### Technical Overview Extensions are smart contracts that: 1. **Inherit from MExtension.sol**: A base contract providing core wrap/unwrap functionality 2. **Implement Custom Logic**: Add your unique features, access controls, and yield distribution 3. **Gain Earner Approval**: Get governance approval to earn yield 4. **Deploy Anywhere**: Launch on Ethereum, L2s, or any supported chain **Key Functions Every Extension Must Implement:** - `wrap(recipient, amount)`: Convert to your extension token - `unwrap(recipient, amount)`: Convert your extension token back - Custom yield claiming/distribution logic (your choice how this works) ### Getting Started Ready to build your own stablecoin? The [Build section](https://docs.m0.org/build/overview) provides everything you need: - **Model Selection Guide**: Choose the right template for your use case - **Step-by-Step Deployment**: Deploy and configure your extension - **Earner Approval Process**: Get governance approval for yield earning - **Integration Examples**: Real code examples and best practices ## SwapFacility Deep Dive ### Architecture Overview SwapFacility serves as the universal router for the M0 Extension ecosystem, enabling atomic conversions between any approved M0 Extensions. The contract maintains minimal state while relying on the TTG governance system for extension approval and access control. #### Core Dependencies - **M Token Base**: The foundational rebasing token that backs all extensions - **TTG Registrar**: Governance-controlled registry that maintains the approved earners list - **ReentrancyLock**: Advanced protection using transient storage for atomic operations ### Swap Operations #### Extension-to-Extension Swaps ```solidity function swap(address extensionIn, address extensionOut, uint256 amount, address recipient) external ``` **Process Flow:** 1. Validates both extensions are approved earners via `_revertIfNotApprovedExtension()` 2. Transfers extension tokens from user to SwapFacility 3. Calls `extensionIn.unwrap()` to convert to $M 4. Measures actual `$M` received (accounts for rounding differences in v1 extensions) 5. Approves and calls `extensionOut.wrap()` to mint target extension tokens #### Direct `$M` Token Operations ```solidity function swapInM(address extensionOut, uint256 amount, address recipient) external function swapOutM(address extensionIn, uint256 amount, address recipient) external ``` **Access Control Differences:** - `swapInM`: Anyone can convert `$M` to extensions - `swapOutM`: Requires `M_SWAPPER_ROLE` or permission for specific extension ### Permission System #### Two-Tier Extension Model 1. **Standard Extensions**: Open for general extension-to-extension swaps 2. **Permissioned Extensions**: Require special authorization for `$M` conversions ```solidity function setPermissionedExtension(address extension, bool permissioned) external function setPermissionedMSwapper(address extension, address swapper, bool allowed) external ``` #### Role-Based Access - `DEFAULT_ADMIN_ROLE`: Contract administration and upgrades - `M_SWAPPER_ROLE`: Global permission for `swapOutM` operations on standard extensions ### Security Features #### Advanced Reentrancy Protection The contract inherits from `ReentrancyLock` which implements transient storage-based locking: ```solidity modifier isNotLocked() { if (Locker.get() != address(0)) revert ContractLocked(); address caller_ = isTrustedRouter(msg.sender) ? IMsgSender(msg.sender).msgSender() : msg.sender; Locker.set(caller_); _; Locker.set(address(0)); } ``` #### Trusted Router System For complex integrations (like UniswapV3SwapAdapter), trusted routers can specify the original caller: - Direct calls: `msg.sender` is stored as the locker - Trusted router calls: `IMsgSender(msg.sender).msgSender()` retrieves the original user Extensions can call `swapFacility.msgSender()` to get the true transaction initiator. #### Governance Integration All extension approvals are checked against the TTG Registrar: ```solidity function _isApprovedEarner(address extension) private view returns (bool) { return IRegistrarLike(registrar).get(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, extension); } ``` ### Gas Optimization #### Permit Integration All swap functions include permit variants supporting both EIP-2612 and EIP-712 signatures: ```solidity function swapWithPermit(..., uint256 deadline, uint8 v, bytes32 r, bytes32 s) external function swapWithPermit(..., uint256 deadline, bytes calldata signature) external ``` #### Balance Tracking Precision For compatibility with Wrapped `$M` v1, the contract measures actual balance changes: ```solidity uint256 mBalanceBefore = _mBalanceOf(address(this)); IMExtension(extensionIn).unwrap(address(this), amount); amount = _mBalanceOf(address(this)) - mBalanceBefore; ``` ### Technical Implementation #### Immutable Architecture ```solidity address public immutable mToken; address public immutable registrar; ``` Core dependencies are immutable for security, while operational parameters remain upgradeable. #### State Management The contract maintains minimal state: - `permissionedExtensions`: Extensions requiring special authorization - `permissionedMSwappers`: Authorized swappers for specific permissioned extensions #### Event System ```solidity event Swapped(address indexed extensionIn, address indexed extensionOut, uint256 amount, address indexed recipient); event SwappedInM(address indexed extensionOut, uint256 amount, address indexed recipient); event SwappedOutM(address indexed extensionIn, uint256 amount, address indexed recipient); ``` ### Economic Guarantees SwapFacility maintains perfect 1:1 value relationships by design: - No slippage or trading fees - Atomic execution prevents arbitrage - All conversions preserve dollar parity - Extensions remain fully backed by eligible collateral The contract creates implicit liquidity between all M0 Extensions without requiring separate AMM pools, enabling seamless cross-extension value transfer. # Limit Order Protocol The Limit Order Protocol is the onchain settlement layer for M0's Liquidity Delivery Network. It provides a trustless, intent-based protocol for exchanging tokens across chains with support for partial fills, destination-chain cancellation, and permissionless refunds after expiry. ## Overview The Limit Order Protocol allows users to submit same-chain or cross-chain limit orders to exchange one token for another. While the primary use case is stablecoin orchestration - swapping between USDC and M0 extensions - the protocol is asset-agnostic. ### Key Design Principles - **Trustless execution** - No trust assumptions on users or solvers; only bridge contracts and messaging protocols are trusted parties - **Partial fills** - Orders can be filled incrementally, allowing solvers to cycle inventory for large orders - **Deterministic pricing** - Users specify exact `amountIn` and `amountOut` - **Cross-chain native** - Orders are created on the source chain but can be filled on any configured destination - **Secure order cancellations** - Cancels are initiated on the destination chain so that in-flight fills and cancellations cannot race ## Architecture The system consists of: - **`OrderBook` contracts** deployed on each supported chain - **Portal/Messenger** for cross-chain communication via [Portal V2](https://docs.m0.org/protocol/portal-v2) - **Solvers** that monitor and fill orders ### Chain Interactions For **same-chain orders**: 1. User opens order on Chain A 2. Solver fills order on Chain A 3. Tokens are exchanged atomically For **cross-chain orders**: 1. User opens order on Chain A (origin), locking input tokens 2. Solver fills order on Chain B (destination), delivering output tokens to recipient 3. Fill report is sent back to Chain A via the messaging layer 4. Solver receives input tokens on Chain A ## Order Lifecycle ### 1. Order Creation Users create orders by calling `openOrder()` on the `OrderBook` contract on the origin chain. The order specifies: | Parameter | Description | | -------------- | ------------------------------------------------------------- | | `destChainId` | Destination chain where tokens will be delivered | | `tokenIn` | Input token address on origin chain | | `tokenOut` | Output token address on destination chain | | `amountIn` | Amount of input token to exchange | | `amountOut` | Expected amount of output token | | `recipient` | Address to receive tokens on destination chain | | `fillDeadline` | Timestamp by which order must be filled | | `solver` | (Optional) Exclusive solver address, or zero for any solver | | `sender` | Address that owns the order, holding cancel and refund rights | ```solidity struct OrderParams { uint32 destChainId; uint32 fillDeadline; address tokenIn; bytes32 tokenOut; // bytes32 supports non-EVM destinations uint128 amountIn; uint128 amountOut; bytes32 recipient; bytes32 solver; address sender; } ``` When an order is created: - Input tokens are transferred from the funder (`msg.sender`) to the `OrderBook` - A unique `orderId` is generated from the order parameters - The order is stored with status `Created` `OrderParams.sender` lets the address that **pays** for an order differ from the address that **owns** it. Tokens are pulled from `msg.sender` (the funder), while `sender` holds cancellation rights and receives any refund. This allows a wrapper or router contract to fund an order on behalf of an end user without taking custody of the order itself. For the common case where the caller owns its own order, set `sender` to `msg.sender`. Two `openOrderWithPermit()` overloads accept an EIP-2612 permit so a user can approve and open in a single transaction - one taking split `(v, r, s)` values and one taking a packed signature. ### 2. Order Filling Solvers fill orders by calling `fillOrder()` on the destination chain's `OrderBook`: ```solidity function fillOrder( bytes32 orderId_, OrderData calldata orderData_, FillParams calldata fillerParams_ ) external payable returns (bytes32 messageId_); ``` Two further overloads accept `bridgeAdapterArgs_`, or both `bridgeAdapter_` and `bridgeAdapterArgs_`, to route the resulting `FillReport` through a specific Portal V2 bridge adapter. `fillOrder()` validates that the supplied `orderData_` hashes to `orderId_`, that this chain is the destination, that the deadline has not passed, that `orderData_.version` matches the contract `VERSION`, that `createdAt` is not in the future, that `amountOutToFill != 0`, that `originRecipient != 0`, and — if the order names an exclusive solver — that `msg.sender` is that solver. **Partial fills** are supported. The amount actually filled is the lesser of `fillerParams_.amountOutToFill` and the remaining unfilled amount, so a solver can safely over-specify: - A proportional amount of `tokenIn` is released to the solver - Multiple solvers can fill the same order until it is complete **Same-chain fills** release input tokens immediately to the solver's `originRecipient`. `msg.value` must be `0`, otherwise `InvalidMsgValue`. **Cross-chain fills** send a `FillReport` back to the origin chain: - `msg.value` is forwarded to the Portal as the bridge fee - `fillerParams_.refundAddress` receives any bridge overpayment; if zero, it defaults to `msg.sender` - When the report is delivered, `reportFill()` releases input tokens to the solver's `originRecipient` ### 3. Order Completion An order reaches `Completed` status when the full `amountOut` has been filled. ### 4. Cancellation and Refunds Cancellation is a single action, taken on the **destination** chain — the same chain where fills happen, so that a cancel and an in-flight fill resolve against the same state. Cancel an order by calling `cancelOrder()`: ```solidity function cancelOrder(bytes32 orderId_, OrderData calldata orderData_) external payable returns (bytes32 messageId_); ``` As with `fillOrder()`, overloads accept `bridgeAdapterArgs_`, or both `bridgeAdapter_` and `bridgeAdapterArgs_`, to direct the `CancelReport` through a specific bridge. For gasless cancellation, the **recipient** signs an EIP-712 message and anyone can submit it: ```solidity function cancelOrderFor( bytes32 orderId_, OrderData calldata orderData_, bytes calldata signature_ ) external payable returns (bytes32 messageId_); ``` The signature is verified against `orderData_.recipient` and covers the `orderId`, `bridgeAdapter`, and `bridgeAdapterArgs`. Both 65-byte and EIP-2098 compact 64-byte signatures are accepted. Who can cancel depends on the order type and whether the deadline has passed: | Scenario | Who can cancel | | ---------------------------------- | ------------------------------------------- | | Same-chain order, before deadline | The order's `sender` or `recipient` | | Cross-chain order, before deadline | The order's `recipient` only | | Any order, after `fillDeadline` | **Anyone** - enables permissionless refunds | Only the `recipient` can cancel a cross-chain order before the deadline, because the `sender` lives on the origin chain and may not control the same address on the destination. | Order type | What happens on cancel | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Same-chain** (origin = dest) | The unfilled remainder of `amountIn` transfers to the order's `sender` immediately. `msg.value` must be `0`. | | **Cross-chain** | Status becomes `Cancelled` and a `CancelReport` is sent via the Portal. `msg.value` pays the bridge fee, refunded to `msg.sender`. On the origin chain, `reportCancel()` releases the remainder to the `sender`. | ::note Because cross-chain messages can arrive out of order, `reportFill()` accepts fills against orders in either `Created` **or** `Cancelled` status. The invariant `amountInReleased + amountInRefunded <= amountIn` prevents over-distribution, so a fill that was in flight when the cancel landed still settles correctly. :: ### Edge Cases **Concurrent cancel and fill:** If a fill is in-flight when an order is cancelled, the fill can still settle on the origin chain. `reportFill()` accepts orders in `Cancelled` status, and the invariant `amountInReleased + amountInRefunded <= amountIn` prevents over-distribution. ## Order Identification Each order has a unique ID derived from hashing its parameters: ```solidity orderId = keccak256(abi.encodePacked( version, sender, nonce, originChainId, destChainId, createdAt, fillDeadline, tokenIn, tokenOut, amountIn, amountOut, recipient, solver )) ``` This ensures orders are globally unique across all chains, solvers can verify order authenticity without trusting the origin chain, and the same order parameters always produce the same ID. The EVM and SVM implementations produce identical order IDs. ## Solver Integration Solvers monitor `OrderBook` events to discover fillable orders: ```solidity event OrderOpened( bytes32 orderId, address funder, address indexed sender, address tokenIn, uint128 amountIn, uint32 indexed destChainId, bytes32 tokenOut, uint128 amountOut, bytes32 indexed solver, uint32 fillDeadline ); ``` ### Exclusive Solvers If an order specifies a `solver` address, only that address can fill the order. Setting `solver` to zero allows any solver to fill (permissionless racing). ### Fill Strategy Solvers determine their own fill strategy - fill entire orders when inventory allows, partially fill large orders to manage risk, or prioritize orders by profitability or deadline urgency. ### Receiving Tokens For cross-chain fills, solvers specify an `originRecipient` in their fill parameters. This address receives the input tokens on the origin chain after the fill report is processed. ### Bridge Fees For cross-chain fills and cancels, `msg.value` covers the Portal V2 bridge fee. Hyperlane and LayerZero adapters expose an onchain `quote()` function; Wormhole uses its Executor API. The full solver-facing walkthrough lives in the [solver integration guide](https://github.com/m0-foundation/liquidity-delivery/blob/main/guides/solver_integration.md){rel=""nofollow""} in the protocol repository. ## Security Considerations ### Token Compatibility ::warning **Non-standard token risks** The `OrderBook` has known edge cases with non-standard tokens. Review this section carefully before whitelisting tokens. :: | Token Type | Behavior | Severity | Recommendation | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Pausable tokens** (USDC/USDT) | While the token is paused, fills and cancel settlement revert, so an order cannot complete until the token unpauses. Funds are not at risk. | **LOW** | Safe to whitelist - nearly every major stablecoin is pausable. Expect fills and refunds to stall for the duration of a pause. | | **Rebase tokens** | If token rebases downward after order creation, `reportFill()` and `reportCancel()` may revert permanently, locking funds | **HIGH** | Do not use | | **Yield-bearing tokens** | Yield accrues to `OrderBook` contract, not recoverable by user or solver | **MEDIUM** | Use short order lifetimes; configure fee recovery via `MEarnerManager` | | **Fee-on-transfer tokens** | `fillOrder()` reverts via `safeTransferExact` | **MEDIUM** | Do not use | ### Solver Safety ::warning **Solver fund loss risks** The following behaviors can cause fund loss for solvers. :: **Dust fills with decimal mismatch:** When `tokenIn` has fewer decimals than `tokenOut`, very small fills can round to zero `amountIn`. The pro-rata formula is: ```text amountInToRelease = (amountIn * amountOutToFill) / amountOut ``` Solvers should validate minimum fill amounts to avoid zero-value releases. ### Rounding Behavior Pro-rata calculations for partial fills always round down (floor): - Users receive at least their proportional share - Solvers may receive slightly less than the theoretical maximum - Very small fills relative to decimal differences may round to zero ### Messenger Trust The only trusted component is the messenger contract that relays fill and cancel reports between chains. M0 uses its [Portal V2](https://docs.m0.org/protocol/portal-v2) infrastructure for secure cross-chain messaging. ## Deployments The `OrderBook` is deployed behind an OpenZeppelin `TransparentUpgradeableProxy` whose address is derived via CREATE3, giving it the **same address on every EVM chain**. Solana runs an equivalent `order_book` program. ::card --- icon: i-lucide-book-marked title: OrderBook addresses to: https://docs.m0.org/resources/addresses/m0-platform --- Deployed `OrderBook` addresses, listed under each chain alongside the rest of the M0 platform contracts. :: The `OrderBook` uses the same internal chain IDs as [Portal V2](https://docs.m0.org/protocol/portal-v2) in its cross-chain payloads. These match standard EVM chain IDs on EVM networks; bridge adapters translate them into the underlying messaging-network IDs. | Network | Internal Chain ID | | ------------ | ----------------- | | Ethereum | `1` | | Base | `8453` | | Arbitrum One | `42161` | | Monad | `143` | | Moca Chain | `2288` | | Citrea | `4114` | | Rise | `4153` | | Solana | `1399811149` | ::warning **A deployment is not the same as a routable chain** These are the chains where the `OrderBook` contract itself is deployed — useful if you are integrating the contract directly. The [Orchestration API](https://docs.m0.org/api-reference/orchestration/overview) enables limit order routing on a **subset** of these chains, and reaches other chains through Portal V2 and the M0 Swap Facility instead. Before you rely on a limit order route, check [`/supported-assets`](https://docs.m0.org/api-reference/orchestration/supported-assets) rather than this list. :: ## Contract Reference The Limit Order Protocol is implemented in the [`OrderBook.sol`](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} contract. ### Key Functions | Function | Description | | --------------------------- | ------------------------------------------------------------- | | `openOrder()` | Create a new limit order | | `openOrderWithPermit()` | Create an order with an EIP-2612 permit (2 overloads) | | `fillOrder()` | Fill an order, fully or partially (3 overloads) | | `cancelOrder()` | Cancel an order on the destination chain (3 overloads) | | `cancelOrderFor()` | Cancel gaslessly with the recipient's signature (3 overloads) | | `reportFill()` | Process a cross-chain fill report (portal only) | | `reportCancel()` | Process a cross-chain cancel report (portal only) | | `setDestinationSupported()` | Enable or disable a destination chain (`DEFAULT_ADMIN_ROLE`) | | `pause()` / `unpause()` | Pause or resume order actions (`PAUSER_ROLE`) | ### View Functions | Function | Description | | -------------------------- | ------------------------------------------------- | | `getOrder()` | Get stored order details by ID | | `getOrderData()` | Get the `OrderData` payload solvers need to fill | | `getOrderId()` | Compute an order ID from `OrderData` | | `getFilledAmounts()` | Get filled and refunded amounts for an order | | `getSenderNonce()` | Get the next nonce for an address | | `isDestinationSupported()` | Check whether a chain is a supported destination | | `getCancelOrderDigest()` | Get the EIP-712 digest for a gasless cancellation | | `VERSION` | Order format version (`1`) | | `portal` | Immutable Portal V2 address used for messaging | ## Data Structures ### OrderStatus ```solidity enum OrderStatus { DoesNotExist, // Order has never been created on this chain Created, // Order is active and fillable Cancelled, // Order was cancelled; in-flight fills may still report Completed // Order fully filled } ``` ### Order Complete data about an order originated on this chain (stored on the origin chain only): ```solidity struct Order { OrderStatus status; uint16 version; address sender; uint64 nonce; uint32 destChainId; uint32 createdAt; uint32 fillDeadline; address tokenIn; bytes32 tokenOut; uint128 amountIn; uint128 amountOut; bytes32 recipient; bytes32 solver; } ``` ### OrderData The cross-chain payload used to compute the order ID and to fill or cancel an order on the destination chain. All addresses are 32 bytes and timestamps are 64-bit so the encoding is well-defined across chains: ```solidity struct OrderData { uint16 version; bytes32 sender; uint64 nonce; uint32 originChainId; uint32 destChainId; uint64 createdAt; uint64 fillDeadline; bytes32 tokenIn; bytes32 tokenOut; uint128 amountIn; uint128 amountOut; bytes32 recipient; bytes32 solver; } ``` ### FillParams ```solidity struct FillParams { uint128 amountOutToFill; bytes32 originRecipient; // receives released tokenIn on the origin chain bytes32 refundAddress; // bridge-fee refund address; zero == msg.sender } ``` ### FilledAmounts Tracked on both origin and destination chains: ```solidity struct FilledAmounts { uint128 amountInRefunded; // released back to the order sender uint128 amountInReleased; // released to solver(s) after fills uint128 amountOutFilled; // tokenOut delivered to the recipient } ``` ### FillReport ```solidity struct FillReport { bytes32 orderId; uint128 amountInToRelease; uint128 amountOutFilled; bytes32 originRecipient; bytes32 tokenIn; } ``` ### CancelReport ```solidity struct CancelReport { bytes32 orderId; bytes32 orderSender; bytes32 tokenIn; uint128 amountInToRefund; } ``` ## Events ```solidity // Order lifecycle event OrderOpened( bytes32 orderId, address funder, address indexed sender, address tokenIn, uint128 amountIn, uint32 indexed destChainId, bytes32 tokenOut, uint128 amountOut, bytes32 indexed solver, uint32 fillDeadline ); event OrderFilled( bytes32 indexed orderId, address indexed solver, uint128 amountInToRelease, uint128 amountOutFilled, bytes32 indexed messageId // zero for same-chain fills ); event OrderCompleted(bytes32 orderId); // Cancellation and refunds event OrderCancelled(bytes32 indexed orderId, bytes32 indexed messageId); event RefundClaimed( bytes32 indexed orderId, address indexed sender, uint128 amountInRefunded ); // Cross-chain reports received on the origin chain event FillReported( bytes32 indexed orderId, address indexed originRecipient, uint128 amountInToRelease, uint128 amountOutFilled ); event CancelReported(bytes32 indexed orderId); // Configuration event DestinationSupportUpdated(uint32 indexed destChainId, bool isSupported); ``` ## Error Codes | Error | Description | | ------------------------- | --------------------------------------------------------------- | | `AmountInZero` | Order input amount cannot be zero | | `AmountOutZero` | Order output amount cannot be zero | | `FillAmountZero` | Fill amount must be greater than zero | | `InvalidDeadline` | Fill deadline is in the past | | `InvalidDestinationChain` | Destination chain is not supported, or is not the current chain | | `InvalidMsgValue` | `msg.value` must be zero for same-chain fills and cancels | | `InvalidOrderStatus` | Order is not in a valid status for this operation | | `InvalidOrderVersion` | Order version doesn't match the contract version | | `InvalidRecipient` | Recipient or `originRecipient` is the zero address | | `InvalidReport` | Fill or cancel report data is invalid, or breaks an invariant | | `InvalidReportSource` | Report arrived from a chain other than the order's destination | | `InvalidSolver` | Solver cannot be the same address as the recipient | | `InvalidTimestamp` | Order `createdAt` is in the future | | `NotAuthorized` | Caller is not authorized for this operation | | `OrderAlreadyExists` | An order with this ID already exists | | `OrderExpired` | Order fill deadline has passed | | `OrderIdMismatch` | Computed order ID doesn't match the provided ID | | `SameTokenOrder` | Same-chain orders cannot use the same token in and out | | `ZeroAdmin` | Admin address cannot be zero at initialization | | `ZeroPauser` | Pauser address cannot be zero at initialization | | `ZeroPortal` | Portal address cannot be zero at deployment | | `ZeroSender` | Order `sender` cannot be the zero address | ## Related - [Accessing Liquidity](https://docs.m0.org/build/accessing-liquidity) - Overview of M0's liquidity infrastructure - [Portal V2](https://docs.m0.org/protocol/portal-v2) - Cross-chain bridge and messaging system - [Orchestration API](https://docs.m0.org/api-reference/orchestration/overview) - API for requesting quotes and transaction payloads - [Source Code](https://github.com/m0-foundation/liquidity-delivery){rel=""nofollow""} - Smart contract repository # M Token ## Overview *M is for Money.* ![M](https://docs.m0.org/images/technical-documentations/m/M.png){.doc-logo} `$M` is an immutable ERC20-compliant token at the heart of the M0 Ecosystem. It serves as the foundational building block for all M0 Extensions. Each unit is backed 1:1 by approved collateral. It implements an innovative dual-balance accounting system: 1. **Non-earning Balances**: Standard ERC20-style token balances that remain static over time - non-rebasing amounts do not increase without external transactions. 2. **Earning Balances**: Token balances that automatically increase in quantity over time through continuous compounding. Holders with earning balances receive additional tokens without requiring any transactions. This rebasing mechanism means users see their token count grow as interest accrues continuously, with their viewable balance updating in real-time (per block). This design separates standard token functionality from interest-bearing capabilities. The M0 Two-Token Governance (TTG) system determines which accounts are eligible to become Earners through an approval process recorded in the [`TTGRegistrar`](https://docs.m0.org/protocol/roles). `$M` interacts closely with several **key components** of the M0 Protocol: - **[`MinterGateway`](https://docs.m0.org/protocol/minting-burning)**: Controls the total supply through secure mint and burn operations. - **[`Rate Models`](https://docs.m0.org/protocol/rate-models)**: External smart contracts that dynamically calculate the interest rates for both Minters and Earners. - **[`TTGRegistrar`](https://docs.m0.org/protocol/roles)**: The governance-controlled parameter store that, among other things, maintains the list of approved Earners and the addresses of the active Rate Models. - **[`DistributionVault`](https://docs.m0.org/protocol/distribution-vault)**: Receives excess yield, which arises when the interest charged to Minters exceeds the yield distributed to Earners. ## Key Features and User Interactions - **Governance-Approved Yield Access**: Selected accounts can earn continuous yield after Two Token Governance (TTG) approval. - **Automatic Yield**: Earning balances grow continuously without requiring any user action. - **Transparency**: All interest calculations occur onchain with fully transparent mechanics. - **Efficiency**: A single token contract handles both stable (non-earning) and yield-generating (earning) use cases. - **Precision**: Advanced mathematical techniques are employed to ensure accurate interest calculations. - **Safety Mechanisms**: Protections are in place to ensure accounts removed from the `APPROVED_EARNERS` list by governance cannot continue earning. ### User Interactions #### Basic Token Operations `$M` implements all standard ERC20 functions and includes additional features: - It has **6 decimal places** of precision for all operations. - Supports standard transfers (`transfer`, `transferFrom`), approvals (`approve`), and balance inquiries (`balanceOf`). - Includes support for EIP-2612 `permit` for gasless approvals. - Includes support for EIP-3009 for transfers with authorization (`transferWithAuthorization`, `receiveWithAuthorization`). #### Earning Status Management Users can interact with the earning functionality through several key functions on the `MToken` contract: - `startEarning()`: Converts an account's regular (non-earning) balance to an earning balance. This function can only be called by an account for itself, and only if that account has been approved as an Earner by M0 Governance. - `stopEarning()`: Converts an account's earning balance back to a regular (non-earning) balance. The account retains all interest accrued up to that point. This can be called by the account owner. - `stopEarning(address account_)`: A safety function that allows anyone to stop the earning status for a specified `account_` if that account is no longer on the governance-approved `APPROVED_EARNERS` list. This ensures that accounts cannot continue to earn yield if their approval is revoked. #### Balance Querying Several methods are available to check different aspects of balances: - `balanceOf(address account)`: Returns the current balance of the account. For earning accounts, this includes all accrued interest up to the current block. - `principalBalanceOf(address account)`: Returns the underlying principal amount for earning accounts. For non-earning accounts, this will be 0. - `isEarning(address account)`: Checks if an account is currently in earning mode. - `totalEarningSupply()`: Returns the total amount of tokens (present value including accrued interest) currently held in earning balances. - `totalNonEarningSupply()`: Returns the total amount of tokens held in non-earning balances. The `totalSupply()` is the sum of `totalEarningSupply()` and `totalNonEarningSupply()`. ## Technical Mechanics ### Dual Balance System The dual balance system is one of the core innovations of the `$M` token, enabling it to function both as a standard stable token and a yield-bearing asset. #### Two Balance Types | Non-Earning | Earning (allowed by governance) | | ---------------------------------------------------- | ------------------------------------------------------------------------------- | | Function exactly like regular ERC20 tokens | Automatically increase in value over time through continuous compounding | | Value remains constant unless explicitly transferred | Stored internally as "principal amounts" that get multiplied by a growing index | | Stored as actual token amounts in the contract | Interest accrues without requiring any transactions or claim process | ### Balance Conversions in `$M` token The `MToken` contract implements a dual accounting system that allows users and developers to leverage both non-earning and earning states. Understanding how the state changes work is crucial. #### Non-earning to Earning Conversion Process When an approved user calls `startEarning()` to convert from non-earning to earning status: 1. The contract first checks if the user is approved as an earner in the M0 Governance system (via the `TTGRegistrar`). 2. The user's current token balance (let's call it `amount`) is read from storage. 3. This `amount` is converted to a smaller "principal amount" using the following calculation: [[[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[c]{.mord.mathnormal}[i]{.mord.mathnormal}[p]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[A]{.mord.mathnormal}[m]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1.9781em;vertical-align:-0.686em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[c]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.mord}]{style="top:-2.314em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[am]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}]{.mord}]{style="top:-3.677em;"}]{.vlist style="height:1.2921em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.686em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}:br where `currentIndex` started at 1.0 (represented as 1e12) and has been growing continuously based on the `EARNER_RATE_MODEL`. 4. The conversion rounds **down** in favor of the protocol (a tiny fraction may be left, contributing to protocol reserves). 5. The user's raw balance in storage (within the `_balances` mapping, specifically `MBalance.rawBalance`) is updated to this `principalAmount`. Their `MBalance.isEarning` flag is set to `true`. 6. Global accounting variables are updated: - `totalNonEarningSupply` is decreased by the original `amount`. - `principalOfTotalEarningSupply` is increased by the `principalAmount`. This `principalAmount` will continually grow in value as the `currentIndex` increases. The user doesn't see their token count change directly in the `rawBalance` storage, but when they check their balance through `balanceOf()`, the contract multiplies their `principalAmount` by the `currentIndex` to show their true balance including all earned interest. #### Earning to Non-earning Conversion Process When a user calls `stopEarning()` to convert from earning to non-earning status: 1. The contract reads the user's current `principalAmount` from storage. 2. This principal is converted back to a "present amount" (actual token value) by multiplying: [[[]{.katex-mathml}[[[]{.strut style="height:0.8778em;vertical-align:-0.1944em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[ese]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[A]{.mord.mathnormal}[m]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[c]{.mord.mathnormal}[i]{.mord.mathnormal}[p]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[A]{.mord.mathnormal}[m]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[c]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} 3. The resulting `presentAmount` includes all interest earned up to that moment. The calculation of `presentAmount` from `principalAmount` is rounded **down** (`_getPresentAmountRoundedDown`). 4. The user's `rawBalance` in storage is updated to this `presentAmount`. Their `MBalance.isEarning` flag is set to `false`. 5. Global accounting variables are updated: - `totalNonEarningSupply` is increased by the `presentAmount`. - `principalOfTotalEarningSupply` is decreased by the `principalAmount`. After this conversion, the user's balance no longer earns interest but now includes all interest accrued up to the conversion point. Their balance will remain static until they perform another transaction or start earning again (if still approved). #### Example to Illustrate Consider a user with 1,000 `$M` tokens in non-earning state, and the `currentIndex` is 1.05 (representing a 5% increase since inception): 1. When they call `startEarning()`: - `principalAmount = 1000 / 1.05 ≈ 952.380952` (actual value depends on 6 decimals for `$M` and 12 decimals for index, then rounded down). Let's say it's `952.380952 * 10^6` as raw principal. - Their storage value for `rawBalance` becomes this principal. - `balanceOf()` returns `(principalAmount * 1.05 * 10^12) / 10^{12} ≈ 1000 * 10^6`. 2. After time passes and the `currentIndex`grows to 1.08: - Their stored `rawBalance` (principal) is still `952.380952 * 10^6`. - `balanceOf()` now returns `(principalAmount * 1.08 * 10^12) / 10^{12} ≈ 1028.571428 * 10^6`. 3. When they call `stopEarning()`: - `presentAmount = (principalAmount * 1.08 * 10^{12}) / 10^{12}` (rounded down). - Their storage value for `rawBalance` becomes this `presentAmount`. - `balanceOf()` now returns exactly this `presentAmount`. ### Transfer Mechanics Transfers in `MToken` handle accounts with different earning statuses: 1. **In-kind Transfers**: Between two accounts with the same earning status. - Between two earning accounts: The transfer involves principal amounts. The amount to be transferred is converted to its principal equivalent (rounded **up** if sender, effectively taking slightly more principal for the given token amount) and then adjusted in the sender's and receiver's principal balances. - Between two non-earning accounts: The transfer is a standard token amount transfer. 2. **Out-of-kind Transfers**: Between accounts with different earning statuses. - From earning to non-earning: The sender's earning balance (principal) is reduced (principal equivalent rounded **up**), and the receiver's non-earning balance (token amount) is increased by the specified token amount. - From non-earning to earning: The sender's non-earning balance (token amount) is reduced, and the receiver's earning balance is increased by the principal equivalent of the token amount (rounded **down**). ### Strategic Rounding The protocol employs consistent rounding rules that slightly favor the protocol to create a small buffer, enhancing system stability and protecting against potential exploitation. These small rounding differences accumulate as protocol reserves. | Operation | From | To | Rounding Rule for Amount Conversion to Principal | Protocol Favored | Internal Function Involved (Illustrative) | | :------------------- | :--------- | :--------- | :--------------------------------------------------------------------------------------------- | :--------------- | :------------------------------------------ | | **Conversions** | | | | | | | Start Earning | Non-Earner | Earner | Principal from present amount rounded **DOWN** | Yes | `_getPrincipalAmountRoundedDown` | | Stop Earning | Earner | Non-Earner | Present amount from principal rounded **DOWN** (for the final balance calculation) | Yes | `_getPresentAmountRoundedDown` | | **Transfers** | | | | | | | Earner to Earner | Earner | Earner | Amount to principal for sender: rounded **UP**; for receiver: (principal transferred directly) | Sender: Yes | `_getPrincipalAmountRoundedUp` (sender) | | Earner to Non-Earner | Earner | Non-Earner | Amount to principal for sender: rounded **UP** | Yes | `_getPrincipalAmountRoundedUp` (sender) | | Non-Earner to Earner | Non-Earner | Earner | Amount to principal for receiver: rounded **DOWN** | Yes | `_getPrincipalAmountRoundedDown` (receiver) | | **Mint/Burn** | | | | | | | Mint to Earner | - | Earner | Amount to principal rounded **DOWN** | Yes | `_getPrincipalAmountRoundedDown` | | Burn from Earner | Earner | - | Amount to principal rounded **UP** | Yes | `_getPrincipalAmountRoundedUp` | *(Note: The table simplifies complex interactions. Refer to the [`MToken.sol`](https://docs.m0.org/protocol/m-token) contract for precise implementation details, especially `_transferOutOfKind`, `_transferAmountInKind`, `_addEarningAmount`, `_subtractEarningAmount`, `_addNonEarningAmount`, `_subtractNonEarningAmount`.)* ### Global Index & Continuous Compounding At the heart of the `$M` token's interest mechanism for earning balances is a global index that efficiently tracks the growth of all earning balances through continuous compound interest. #### Index Mechanism - **Single Global Index**: A single shared growth factor (`latestIndex`) applies to all earning balances, making the system highly gas-efficient. - **Mathematical Relationship**: For an earning account, its `balanceOf()` is effectively `principalAmount * currentIndex`. - **Starting Point**: The index starts at 1.0 (represented as `1e12` internally due to 12 decimal places of precision for the index) and only increases over time. - **Index Storage**: The `MToken` contract inherits from `ContinuousIndexing`, which stores `latestIndex` (uint128) and `latestUpdateTimestamp` (uint40). #### Mathematical Implementation The `MToken` contract implements true continuous compounding: - **Continuous Compounding Formula**: The index is updated using the formula `newIndex = oldIndex * e^(rate * timeElapsed / SECONDS_PER_YEAR)`. - **Interest Accumulation**: Interest compounds with every second that passes, with the `currentIndex()` function calculating the up-to-date index on-demand. - **Exponential Approximation**: The `e^x` function is implemented onchain using a Pade approximant R(4,4) for gas efficiency and precision: [[[]{.katex-mathml}[[[]{.strut style="height:1em;vertical-align:-0.25em;"}[e]{.mord.mathnormal}[(]{.mopen}[x]{.mord.mathnormal}[)]{.mclose}[]{.mspace style="margin-right:0.2778em;"}[≈]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.9043em;vertical-align:-1.1514em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3.0179em;"}[[1]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[2]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.6954em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[28]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[3]{.mord.mtight}[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[2]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[84]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[3]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[1680]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[4]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.mord}]{style="top:-2.2115em;"}[[]{.pstrut style="height:3.0179em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.2479em;"}[[]{.pstrut style="height:3.0179em;"}[[1]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[2]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.6954em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[28]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[3]{.mord.mtight}[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[2]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[84]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[3]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[1680]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[4]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.mord}]{style="top:-3.7529em;"}]{.vlist style="height:1.7529em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.1514em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} - **Rate Conversion**: Rates obtained from the `EARNER_RATE_MODEL` (in basis points) are converted to a scaled format suitable for the exponential calculations. - **Time Scaling**: Rates are scaled by the time elapsed (in seconds) divided by `SECONDS_PER_YEAR` (31,536,000). #### Principal vs. Present Value The contract manages two key value concepts for earning balances: - **Principal Amount**: The base `rawBalance` (uint240, but effectively uint112 for principal part) stored for earning accounts. This is the amount that earns interest. - **Present Amount**: The current value of an earning balance, including all accrued interest, calculated as `principalAmount * currentIndex`. This is what `balanceOf()` returns. Conversion functions handle translations: - `_getPresentAmount(uint112 principalAmount_)` or `_getPresentAmountRoundedDown(uint112 principalAmount, uint128 index_)`: Multiplies principal by the current index. - `_getPrincipalAmountRoundedDown(uint240 presentAmount_)`: Divides present amount by the current index, rounded down. Used when adding to earning accounts. - `_getPrincipalAmountRoundedUp(uint240 presentAmount_)`: Divides present amount by the current index, rounded up. Used when subtracting from earning accounts. #### Index Updates The global earning index (`latestIndex`) in `MToken` is updated by calling `updateIndex()`. This function is typically called: - By the `MinterGateway` during its own `updateIndex()` calls (e.g., during `mintM`, `burnM`, `updateCollateral`, `deactivateMinter`). This ensures synchronization between Minter interest payments and Earner yield accrual. - Internally within `MToken` before operations that change an account's earning status or modify `principalOfTotalEarningSupply`, such as: - `_startEarning()` - `_stopEarning()` - Minting directly to an earning account via `_mint()`. - Burning directly from an earning account via `_burn()`. - Transfers that involve an earning account (`_transferOutOfKind`, `_transferAmountInKind` for earning-to-earning). Each `MToken.updateIndex()` call: 1. Fetches the current earner rate from the `EARNER_RATE_MODEL` (address obtained from `TTGRegistrar`). 2. Calculates the time elapsed since `latestUpdateTimestamp`. 3. Computes the new index: `newIndex = latestIndex * e^(rate * timeElapsed / SECONDS_PER_YEAR)`. 4. Updates `latestIndex` and `latestUpdateTimestamp` in storage. #### Precision and Efficiency - **Fixed-Point Arithmetic**: The index uses 12 decimal places of precision (scaled by `1e12`). `$M` balances have 6 decimals. - **Conservative Rounding**: All rounding strategies are designed to favor protocol safety, with small residuals contributing to protocol reserves. - **Gas Optimization**: The single global index is updated only when necessary and is shared across all earning accounts. - **Overflow Protection**: The index is capped at `type(uint128).max`. Principal amounts are also managed to prevent overflow in calculations. ## Security and Governance Interactions The security and behavior of the `$M` token are deeply intertwined with the M0 Protocol's governance and other core contracts. ### Supply Control - The total supply of `$M` is exclusively controlled by the `MinterGateway` contract. - `MToken.mint(address recipient, uint240 amount)` can only be called by the `minterGateway` address set in `MToken`. - `MToken.burn(address account, uint240 amount)` can only be called by the `minterGateway` address. This ensures that `$M` tokens are only created or destroyed as per the protocol's minting (collateral-backed) and burning (debt-repayment) rules managed by `MinterGateway`. ### Earning Mechanism Governance - **Earner Approval**: Whether an account can switch to an earning balance (`startEarning()`) is determined by M0 Governance. The `MToken` contract checks if an account is an approved earner by querying the `TTGRegistrar` (specifically, the `APPROVED_EARNERS` list or if `EARNERS_LIST_IGNORED` is true). ```solidity // Simplified check from _isApprovedEarner(address account_) function _isApprovedEarner(address account_) internal view returns (bool) { return ttgRegistrarReader.getBool("earners_list_ignored") || ttgRegistrarReader.listContains("earners", account_); } ``` - **Rate Model Control**: The interest rate for earners is determined by the `EARNER_RATE_MODEL`. The address of this model is a governance-controlled parameter stored in the `TTGRegistrar` and read by `MToken` during `updateIndex()`. ### Safety Controls for Earners - The `stopEarning(address account_)` function allows anyone to call it for an `account_` that is *no longer* on the `APPROVED_EARNERS` list. This is a crucial safety mechanism to prevent an account from continuing to accrue yield if its earner status is revoked by governance. ```solidity function stopEarning(address account_) external { if (_isApprovedEarner(account_)) revert IsApprovedEarner(); // Can only be called if account is NOT an approved earner _stopEarning(account_); } ``` ### Index Management and Synchronization - The `MToken.updateIndex()` function is critical for reflecting the correct yield. As mentioned, it's called by `MinterGateway` to ensure synchronization with Minter-side interest accruals and by `MToken` itself during critical state changes. This synchronization is vital for the protocol's economic balance. ### Rounding and Numerical Stability - The consistent use of protocol-favoring rounding rules in conversions and transfers contributes to the robustness of the `$M` token system. These small, accumulated amounts act as a buffer and eventually contribute to protocol reserves (e.g., claimable by the `DistributionVault`). - Explicit overflow checks and the use of libraries like `UIntMath` and `ContinuousIndexingMath` ensure numerical precision and prevent errors during balance and index calculations. ### Immutability The `MToken` contract itself is designed to be immutable, meaning its core logic cannot be upgraded directly. Changes to its behavior (like interest rates or earner eligibility) are managed externally through governance-controlled parameters in the `TTGRegistrar` and updatable Rate Model contracts. # M Token Specification ## Contract overview The `MToken` contract is an immutable ERC20-compliant token with a dual-balance accounting system. It implements continuous compounding for earning accounts via a global index mechanism. - **Decimals:** 6 - **Index precision:** 12 decimal places (scaled by `1e12`) - **Inherits:** `ContinuousIndexing`, ERC20, EIP-2612, EIP-3009 --- ## Key functions ### ERC20 Standard | Function | Description | | -------------------------------------------------------- | -------------------------------------------------------------- | | `transfer(address to, uint256 amount)` | Transfer tokens to an address | | `transferFrom(address from, address to, uint256 amount)` | Transfer tokens on behalf of another address | | `approve(address spender, uint256 amount)` | Approve a spender allowance | | `balanceOf(address account)` | Returns current balance including accrued interest for earners | | `totalSupply()` | Returns `totalEarningSupply() + totalNonEarningSupply()` | ### EIP-2612 Permit | Function | Description | | -------------------------------------------------------------------------------------------------------- | ------------------------------ | | `permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)` | Gasless approval via signature | ### EIP-3009 Transfer with Authorization | Function | Description | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | `transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)` | Transfer via signed authorization | | `receiveWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)` | Receive via signed authorization | ### Earning Management | Function | Description | | ------------------------------- | ------------------------------------------------------------------ | | `startEarning()` | Convert caller's balance to earning mode (must be approved earner) | | `stopEarning()` | Convert caller's balance back to non-earning mode | | `stopEarning(address account_)` | Force-stop earning for an account no longer on the approved list | ### Balance Queries | Function | Returns | | ------------------------------------- | ------------------------------------------------------------- | | `balanceOf(address account)` | Current balance (including accrued interest for earners) | | `principalBalanceOf(address account)` | Underlying principal for earning accounts (0 for non-earners) | | `isEarning(address account)` | Whether the account is in earning mode | | `totalEarningSupply()` | Total tokens in earning balances (present value) | | `totalNonEarningSupply()` | Total tokens in non-earning balances | ### Index Management | Function | Description | | ----------------------- | ------------------------------------------------------------------------- | | `updateIndex()` | Update the global earning index; called by `MinterGateway` and internally | | `currentIndex()` | Returns the current index value (on-demand calculation) | | `latestIndex` | Last stored index value (uint128) | | `latestUpdateTimestamp` | Timestamp of last index update (uint40) | ### Supply Control (MinterGateway only) | Function | Description | | ----------------------------------------- | --------------------------- | | `mint(address recipient, uint240 amount)` | Mint tokens to an address | | `burn(address account, uint240 amount)` | Burn tokens from an address | --- ## Events | Event | Description | | ------------------------------------------------------------------------- | ---------------------------- | | `Transfer(address indexed from, address indexed to, uint256 value)` | Standard ERC20 transfer | | `Approval(address indexed owner, address indexed spender, uint256 value)` | Standard ERC20 approval | | `StartedEarning(address indexed account)` | Account entered earning mode | | `StoppedEarning(address indexed account)` | Account exited earning mode | --- ## Errors | Error | Condition | | ----------------------------------------------------------------------- | --------------------------------------------------------- | | `NotApprovedEarner()` | Account is not on the approved earners list | | `IsApprovedEarner()` | Attempted to force-stop an account that is still approved | | `NotMinterGateway()` | Caller is not the `MinterGateway` | | `InsufficientBalance(address account, uint256 balance, uint256 amount)` | Transfer or burn exceeds balance | --- ## Continuous compounding formula The global index grows via continuous compounding: [[[]{.katex-mathml}[[[]{.strut style="height:0.6944em;"}[[newIndex]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[[oldIndex]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.938em;"}[[e]{.mord.mathnormal}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[rate]{.mord.mtight}]{.mord.text.mtight}[×]{.mbin.mtight}[[timeElapsed]{.mord.mtight}]{.mord.text.mtight}[/]{.mord.mtight}[[SECONDS\_PER\_YEAR]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.113em;margin-right:0.05em;"}]{.vlist style="height:0.938em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} The `e^x` function uses a Padé approximant R(4,4): [[[]{.katex-mathml}[[[]{.strut style="height:1em;vertical-align:-0.25em;"}[e]{.mord.mathnormal}[(]{.mopen}[x]{.mord.mathnormal}[)]{.mclose}[]{.mspace style="margin-right:0.2778em;"}[≈]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.9043em;vertical-align:-1.1514em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3.0179em;"}[[1]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[2]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.6954em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[28]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[3]{.mord.mtight}[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[2]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[84]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[3]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[1680]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[4]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.mord}]{style="top:-2.2115em;"}[[]{.pstrut style="height:3.0179em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.2479em;"}[[]{.pstrut style="height:3.0179em;"}[[1]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[2]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.6954em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[28]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[3]{.mord.mtight}[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[2]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[84]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[3]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[1680]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[4]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.mord}]{style="top:-3.7529em;"}]{.vlist style="height:1.7529em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.1514em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} --- ## Rounding rules | Operation | Rounding | Protocol favored | | ---------------------------------------- | -------- | ---------------- | | Start earning (present → principal) | Down | Yes | | Stop earning (principal → present) | Down | Yes | | Earner-to-earner transfer (sender) | Up | Yes | | Non-earner-to-earner transfer (receiver) | Down | Yes | | Mint to earner | Down | Yes | | Burn from earner | Up | Yes | --- ## Related - [M Token overview](https://docs.m0.org/protocol/m-token) - Full conceptual documentation - [Rate Models](https://docs.m0.org/protocol/rate-models) - How interest rates are determined - [Minting & Burning](https://docs.m0.org/protocol/minting-burning) - Supply control via `MinterGateway` # Minting & Burning (MinterGateway) ## Overview *Where money is born.* The `MinterGateway` contract is the central component within the M0 Protocol responsible for orchestrating the entire lifecycle of `$M`. It governs the processes for minting and burning, manages the offchain collateral system that backs the supply, and tracks the debt obligations of Minters. This contract acts as the primary interface for Minters and Validators, ensuring that issuance is always securely collateralized and transparently managed according to protocol rules defined by M0 Governance. Its key responsibilities include: - Managing Minter activation, deactivation, and operational states. - Overseeing the collateral update process, including verification of offchain assets by Validators. - Enforcing the structured minting and burning procedures. - Calculating and applying interest, debt, and penalties for Minters. - Facilitating the collateral retrieval process for Minters. - Synchronizing its interest index with the `MToken` contract to maintain system-wide consistency. ## Minting and Burning Process The creation (minting) and destruction (burning) of `$M` tokens are strictly controlled by the `MinterGateway`. ### Minting `$M` The minting process is designed with security checkpoints involving a proposal, a delay, and execution: 1. **Update Collateral (Prerequisite)**: Before proposing a mint, a Minter must have an up-to-date and sufficient verified collateral value on record. 2. **Propose Mint**: An active Minter initiates a mint request by calling `proposeMint()`: This creates a mint proposal with a unique `mintId_`. The proposal specifies the amount of `$M` to be minted and the recipient address. This step allows Validators to review pending mints. ```solidity function proposeMint(uint256 amount, address destination) external returns (uint48 mintId_); ``` 3. **Delay Period**: A mandatory delay period (`MINT_DELAY`), set by governance, must pass after a mint is proposed before it can be executed. This window allows Validators to intervene (e.g., cancel the mint or freeze the Minter) if they detect any issues. 4. **Execute Mint**: After the `MINT_DELAY` has elapsed, and before the proposal expires (within `MINT_TTL` - Time To Live, also set by governance), the Minter can execute the mint by calling `mintM()`: This function mints the specified amount of `$M` to the destination address, provided the proposal is still valid, hasn't been canceled, the Minter isn't frozen, and the mint doesn't violate collateralization requirements. ```solidity function mintM(uint256 mintId) external; ``` The `MINT_DELAY` and `MINT_TTL` mechanisms create a secure time window for oversight, preventing immediate, unreviewed mints and ensuring proposals don't remain executable indefinitely. ### Burning `$M` `$M` tokens can be burned to reduce a Minter's outstanding debt. Any `$M` holder can initiate a burn against a specific Minter's debt: ```solidity function burnM(address minter, uint256 amount) external; ``` - **For Active Minters**: Burning `$M` reduces the principal of their interest-accruing debt (`activeOwedM`). - **For Deactivated Minters**: Burning `$M` reduces their fixed `inactiveOwedM`. This serves as an informal liquidation mechanism, allowing the market to help settle the obligations of a Minter who has exited the system. The `_repayForDeactivatedMinter` internal function handles this logic. ## Collateral System `$M` is backed by offchain collateral, typically real-world assets like U.S. Treasury Bills held in Special Purpose Vehicles (SPVs). The `MinterGateway` manages the onchain representation and verification of this collateral. ### Offchain Backing & Onchain Representation Minters are responsible for maintaining sufficient eligible collateral offchain. The value of this collateral is reported onchain through the `MinterGateway`. ### Validator Verification Trusted, independent Validators play a crucial role in verifying the Minters' offchain collateral. They periodically assess the collateral and provide cryptographic signatures attesting to its value. ### Update Process Minters must regularly update their onchain collateral value to reflect the current state of their offchain reserves. This is done by calling the `updateCollateral` function: ```solidity function updateCollateral( uint256 collateral, uint256[] calldata retrievalIds, bytes32 metadataHash, address[] calldata validators, uint256[] calldata timestamps, bytes[] calldata signatures ) external returns (uint40 minTimestamp_); ``` Key aspects of the collateral update process: - **Frequency**: Minters must update their collateral within a governance-defined interval (e.g., `UPDATE_COLLATERAL_INTERVAL`, typically daily). Failure to do so results in their onchain collateral value being considered zero until a new valid update is provided. - **Validator Signatures**: A collateral update requires signatures from multiple approved Validators, meeting a minimum threshold set by governance (e.g., `UPDATE_COLLATERAL_THRESHOLD`). - Signatures must be provided with validator addresses in ascending order to prevent duplicates. - Each signature includes a timestamp which must be newer than the last signature from that specific validator for that Minter and must not be in the future. This prevents replay attacks. - Signatures use EIP-712 typed data specific to each validator. - **Collateral Expiration**: If a Minter fails to update their collateral within the `UPDATE_COLLATERAL_INTERVAL`, their effective onchain collateral value is treated as zero for all protocol calculations, potentially leading to penalties or an inability to mint. - **Collateralization Caps**: Minters must maintain a collateralization ratio above the governance-set `MINT_RATIO`. This ratio dictates the maximum amount of `$M` a Minter can have outstanding relative to their verified collateral. - **Earliest Update Timing**: A new collateral update's effective timestamp must be greater than: - The Minter's last update timestamp. - The timestamp of the Minter's latest proposed collateral retrieval. - `block.timestamp - UPDATE_COLLATERAL_INTERVAL` to ensure updates are reasonably current. ## Collateral Retrieval Minters can retrieve excess collateral from their backing SPV, provided the retrieval does not compromise their ability to back their outstanding `$M` debt. This process is managed onchain through the `MinterGateway` and requires Validator oversight. 1. **Propose Retrieval**: The Minter initiates a request to retrieve a specific amount of collateral by calling `proposeRetrieval()`: ```solidity function proposeRetrieval(uint256 collateral) external returns (uint48 retrievalId_); ``` :brThis creates a pending retrieval proposal with a unique, sequential `retrievalId_`. The proposed retrieval amount is tracked in the `_pendingCollateralRetrievals` mapping and is immediately deducted from the Minter's effective collateral value for all ongoing calculations, ensuring the Minter remains adequately collateralized. The system prevents proposals that would lead to undercollateralization. 2. **Validator Inclusion & Resolution**: For a pending retrieval to be finalized, it must be explicitly included and approved by Validators in a subsequent `updateCollateral` transaction. - When Validators sign off on a Minter's collateral update, their signature digest includes the list of `retrievalIds` they are approving for resolution. - During the `updateCollateral` call, the `MinterGateway` processes these approved `retrievalIds`. The corresponding amounts are then considered formally retrieved, and their values are no longer deducted from the Minter's available collateral. This multi-step process ensures that collateral can only be effectively retrieved with explicit, signed approval from Validators as part of a standard, verified collateral update, maintaining the integrity of `$M`'s backing. Multiple retrieval proposals can be resolved within a single `updateCollateral` transaction. ## Debt, Interest, and Penalties for Minters Minters incur debt for the `$M` tokens they mint and are charged interest on this debt. The `MinterGateway` manages this system. ### Debt and Interest - **Debt Accrual**: When a Minter mints `$M`, they incur an equivalent amount of debt. - **Continuous Indexing**: Minter debt, similar to `MToken` earning balances, is tracked using a continuous indexing system. The debt is stored as a principal amount that grows over time as interest accrues. - **Interest Rate Model**: The interest rate charged to Minters (`MINTER_RATE_MODEL`) is determined by an external smart contract, the address of which is set by M0 Governance and stored in the `TTGRegistrar`. - **`activeOwedM`**: This term represents the current total value of an active Minter's obligations, including all accrued interest. It is calculated by applying the current minter index to their principal debt. ### Total Owed `$M` Accounting The `MinterGateway` tracks various categories of Minter debt: - **Active Owed `$M`**: The sum of interest-accruing debt from all active Minters. - **Inactive Owed `$M`**: The sum of fixed, non-interest-accruing debt from all deactivated Minters. - **Total Owed `$M`**: The aggregate of `activeOwedM` and `inactiveOwedM`. - **Excess Owed `$M`**: The difference between `totalOwedM` (interest generated from Minters) and the total supply of `$M` (which grows at the earner rate). This excess, if positive, represents protocol revenue and is minted directly to the [DistributionVault](https://docs.m0.org/protocol/distribution-vault) (TTGVault) during index updates. ### Penalties The protocol imposes penalties on Minters for failing to adhere to specific operational requirements, ensuring system discipline: 1. **Missed Collateral Updates Penalty**: If a Minter fails to update their collateral within the `UPDATE_COLLATERAL_INTERVAL`, a penalty is applied. - Calculation: `PENALTY_RATE * principalOfActiveOwedM * missedIntervals`. - This penalty is charged once per missed interval. 2. **Undercollateralization Penalty**: If a Minter's `activeOwedM` exceeds the maximum allowed by their verified collateral and the `MINT_RATIO`, they are penalized. - Calculation: `PENALTY_RATE * principalOfExcessOwedM * (timeSpan / UPDATE_COLLATERAL_INTERVAL)`. - This penalty is proportional to the amount and duration of the undercollateralization. Penalties are added to the Minter's debt. ## Minter Lifecycle and Management Minters are permissioned entities that interact with the `MinterGateway` to issue and manage `$M`. Their lifecycle within the protocol is characterized by several distinct statuses: 1. **Approved**: Minters are initially whitelisted by the M0 Two-Token Governance (TTG) system. At this stage, they are recognized by the protocol but cannot yet perform minting operations. 2. **Active**: An approved Minter must explicitly activate their status by calling `activateMinter()` on the `MinterGateway`. ```solidity function activateMinter(address minter_) external; ``` This function can only be called by the Minter themselves and transitions them to an operational state, allowing them to propose collateral updates and mint `$M`. The active status is stored locally within `MinterGateway` for gas efficiency. 3. **Frozen**: An active Minter can be temporarily restricted from minting `$M` by Validators (via `freezeMinter()`). During this state, the Minter can still burn `$M` to reduce their debt and update their collateral but cannot propose new mints. The freeze duration is determined by governance. 4. **Deactivated**: A Minter can be deactivated if they are removed from the governance-approved list. Deactivation is a permanent state, initiated by a call to `deactivateMinter()`. ```solidity function deactivateMinter(address minter_) external returns (uint240 inactiveOwedM_); ``` This function can be called if the Minter is no longer approved by TTG. Upon deactivation: - The Minter's outstanding debt, including accrued interest, is converted into a fixed `inactiveOwedM` amount which no longer accrues interest. - The Minter can no longer mint `$M` or propose collateral retrievals. - Their collateral remains to back their `inactiveOwedM`. - A deactivated Minter cannot be reactivated. This permanence optimizes gas by eliminating further status checks against the `TTGRegistrar`. ## Validator Roles and Security Controls Validators are independent, trusted entities that form a critical security layer for the minting and burning processes within the `MinterGateway`. Their responsibilities and powers include: ### Core Responsibilities - **Collateral Verification**: Validators verify Minters' offchain collateral and provide cryptographic signatures for `updateCollateral` transactions. These signatures attest to the value and existence of the assets backing `$M`. - **Timestamping**: Signatures from Validators must include a secure timestamp to prevent replay attacks and ensure the freshness of collateral data. - **Enabling Offchain-Onchain Bridge**: Validators act as the crucial link ensuring the onchain representation of collateral accurately reflects the state of offchain assets. ### Validator Signature Verification The signature verification system includes several security features: - **Multi-signature Requirement**: Requires multiple validator signatures for collateral updates. - **Ordered Validator Addresses**: Validator addresses must be provided in ascending order to prevent duplicates. - **Timestamp Validation**: Each signature includes a timestamp that must be: - Newer than the last signature from that validator for that minter. - Not in the future. - Not zero. - **Replay Protection**: The system tracks the last used timestamp for each validator-minter pair. - **Signature Format**: Uses EIP-712 typed data signatures that are validator-specific. ### Security Controls & Emergency Powers Validators are empowered with specific functions to intervene and protect the protocol: 1. **Cancel Mint Proposal**: If Validators deem a pending mint proposal to be suspicious, invalid, or potentially harmful to the protocol, they can cancel it before execution: ```solidity function cancelMint(address minter, uint256 mintId) external onlyApprovedValidator; ``` :brThis action deletes the mint proposal, preventing the Minter from executing it. 2. **Freeze Minter**: Validators can temporarily suspend a Minter's ability to mint new `$M` tokens: ```solidity function freezeMinter(address minter) external onlyApprovedValidator returns (uint40 frozenUntil_); ``` :brThis "freezes" the Minter for a duration specified by the governance parameter `MINTER_FREEZE_TIME`. A frozen Minter can still burn `$M` and update collateral but cannot propose or execute new mints until the freeze period expires. These controls allow Validators to act as a rapid response mechanism against potential threats or misbehavior related to `$M` issuance. ## Timing Constraints The system enforces several timing-related constraints: - **Collateral Update Frequency**: Minters must update at least once per `UPDATE_COLLATERAL_INTERVAL`. - **Mint Delay**: Proposals must wait `MINT_DELAY` seconds before execution. - **Mint TTL**: Proposals expire after `MINT_TTL` seconds. - **Earliest Collateral Update**: The timestamp for a new update must be greater than: - The last update timestamp. - The latest proposed retrieval timestamp. - `block.timestamp - UPDATE_COLLATERAL_INTERVAL`. ## Security Mechanisms The MinterGateway implements several safety features: - **Mint Delay & TTL (Time-To-Live)**: Creates a time window for validators to review and potentially cancel mints. - **Collateralization Caps**: Enforces maximum mint-to-collateral ratios (set by governance). - **Multi-signature Requirement**: Requires multiple validator signatures for collateral updates. - **Strictly Ordered Validations**: Validator addresses must be provided in ascending order. - **Timestamp Verification**: Prevents reuse of signatures and ensures freshness of collateral data. - **Excess Yield Distribution**: Any yield generated by minters beyond what's distributed to earners goes to the TTG Vault (also called the [DistributionVault](https://docs.m0.org/protocol/distribution-vault)). ## Emergency Measures For extreme circumstances, the protocol includes additional safeguards: ### Mint Proposal Cancellation Validators can cancel suspicious mint proposals before they're executed, allowing them to quickly intervene if they detect potentially harmful minting activity. ```solidity function cancelMint(address minter_, uint256 mintId_) external onlyApprovedValidator { uint48 id_ = _mintProposals[minter_].id; if (id_ != mintId_ || id_ == 0) revert InvalidMintProposal(); delete _mintProposals[minter_]; emit MintCanceled(id_, minter_, msg.sender); } ``` ### Validator Freeze Validators can freeze minters to stop them from minting tokens for a governance-defined period: ```solidity function freezeMinter(address minter_) external onlyApprovedValidator returns (uint40 frozenUntil_) { unchecked { _minterStates[minter_].frozenUntilTimestamp = frozenUntil_ = uint40(block.timestamp) + minterFreezeTime(); } emit MinterFrozen(minter_, frozenUntil_); } ``` ### Minter Deactivation Governance can completely deactivate minters who have been removed from the approved list: ```solidity function deactivateMinter(address minter_) external onlyActiveMinter(minter_) returns (uint240 inactiveOwedM_) { if (isMinterApproved(minter_)) revert StillApprovedMinter(); // Implementation details for deactivation process } ``` Once deactivated, a minter cannot be reactivated, providing a permanent resolution for problematic minters. ### Open Redemption Anyone can burn `$M` tokens to repay a deactivated minter's debt: ```solidity function _repayForDeactivatedMinter(address minter_, uint240 maxAmount_) internal returns (uint240 amount_) { amount_ = UIntMath.min240(inactiveOwedMOf(minter_), maxAmount_); unchecked { _rawOwedM[minter_] -= amount_; totalInactiveOwedM -= amount_; } } ``` This creates an informal liquidation mechanism for deactivated minters. ## Index Synchronization with `$M` Token The `MinterGateway` and the `MToken` contract both utilize a continuous indexing mechanism to track the accrual of interest -- for Minter debt in `MinterGateway` and for earner balances in `MToken`. It is crucial that these indices are synchronized. ### Synchronization Mechanism The `MinterGateway` ensures that whenever its own interest index for Minter obligations is updated, the `MToken`'s earning index is also updated simultaneously. This is achieved within the `MinterGateway.updateIndex()` function: ```solidity function updateIndex() public override(IContinuousIndexing, ContinuousIndexing) returns (uint128 index_) { uint240 excessOwedM_ = excessOwedM(); if (excessOwedM_ > 0) IMToken(mToken).mint(ttgVault, excessOwedM_); // Mint excess to TTG Vault // First, update the MinterGateway's own index and rate (for minter debt) index_ = super.updateIndex(); // Then, explicitly trigger an update of the MToken's index and rate (for earner yield) IMToken(mToken).updateIndex(); // Emit an event if this MinterGateway instance is also the MToken's MinterGateway // (This is relevant if MToken directly calls this contract for its own updates) if (address(this) == IMToken(mToken).minterGateway()) { emit MTokenIndexUpdated(IMToken(mToken).currentIndex(), IMToken(mToken).currentRate()); } } ``` This synchronized update ensures that the calculation of interest paid by Minters and yield distributed to Earners remains consistent and balanced across the protocol, reflecting the latest rates and system state. ### Triggers for Index Updates The `updateIndex()` function in `MinterGateway` (which in turn calls `MToken.updateIndex()`) is invoked during several key operations, ensuring indices are frequently updated: - When a Minter updates their collateral via `updateCollateral()`. - When `$M` tokens are minted via `mintM()`. - When `$M` tokens are burned via `burnM()`. - When a Minter is deactivated via `deactivateMinter()`. - When `MinterGateway.updateIndex()` is explicitly called by an external party. This frequent synchronization is vital for the accurate accounting of debts, yields, and the overall economic stability of the M0 Protocol. # Portal V2 ## Overview Portal V2 is M0's cross-chain bridge and messaging system. It serves two core functions in the M0 ecosystem: 1. **Token bridging** - Transfers M0 extension tokens between all connected chains. 2. **Protocol state propagation** - Synchronizes the `$M` earning index, governance registrar values, and earner lists from Ethereum to all connected chains, and relays fill and cancel reports for the [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol). ### Key Design Principles - **Bridge-agnostic core** - The Portal has no knowledge of any specific messaging protocol; all delivery is abstracted behind a common `IBridgeAdapter` interface - **Per-chain adapter selection** - An operator configures a default adapter for each destination chain, and senders can override it per message - **Hub-and-spoke with risk isolation** - Ethereum is the hub and single source of truth; each spoke is either isolated (hub-only transfers, with per-spoke principal accounting) or connected (spoke-to-spoke transfers allowed) - **Cross-VM compatibility** - Payloads use `bytes32` addresses, `uint32` chain IDs, and `uint128` amounts so the same encoding works on EVM and non-EVM chains - **Permissionless state relay** - Anyone can pay to push the latest index, registrar values, or earner Merkle root to a spoke chain ## Architecture ### Hub Chain (Ethereum) The `HubPortal` contract is deployed on Ethereum, where `$M` is natively issued and governed. It: - **Locks** tokens when bridging to spoke chains and **releases** them when tokens are bridged back - **Tracks bridged principal** per isolated spoke, so it can never release more than was locked for that spoke - **Broadcasts** the earning index, registrar key-value pairs, registrar list updates, and earner Merkle roots ### Spoke Chains `SpokePortal` contracts are deployed on every other supported chain. They: - **Mint** `$M` when tokens arrive from the hub or another spoke, and **burn** it when tokens are sent away - **Apply** index and registrar updates received from the hub; if a token transfer carries a newer index, the local index is updated as part of the mint ### Bridge Adapters Bridge adapters are standalone contracts that handle message delivery through a specific cross-chain protocol. The Portal only interacts with the adapter interface: ```solidity interface IBridgeAdapter { /// @notice The Portal this adapter serves. function portal() external view returns (address); /// @notice Returns the fee for delivering a message to the destination chain. function quote( uint32 destinationChainId, uint256 gasLimit, bytes memory payload ) external view returns (uint256 fee); /// @notice Sends a message to the destination chain. function sendMessage( uint32 destinationChainId, uint256 gasLimit, bytes32 refundAddress, bytes memory payload ) external payable; } ``` Three adapters are currently implemented: | Adapter | Protocol | Delivery model | | ------------------------ | ------------ | ---------------------------------------------------- | | `HyperlaneBridgeAdapter` | Hyperlane | Mailbox dispatch with ISM validation | | `WormholeBridgeAdapter` | Wormhole | Core Bridge publication with Executor-based delivery | | `LayerZeroBridgeAdapter` | LayerZero V2 | Endpoint dispatch with DVN validation | Each adapter maintains two mappings, configured by its operator: - **Peers** (`setPeer()`) - the adapter address on each remote chain. Outgoing messages target the peer; incoming messages are only accepted from it. - **Chain IDs** (`setBridgeChainId()`) - a one-to-one mapping between M0's internal chain IDs and the provider's own scheme (Hyperlane domains, LayerZero endpoint IDs, Wormhole chain IDs). Some adapters take extra arguments through the `bridgeAdapterArgs` parameter on send functions: | Adapter | `bridgeAdapterArgs` | | --------- | --------------------------------------------------------------------------- | | Hyperlane | Unused - pass `""` | | LayerZero | Unused - pass `""` | | Wormhole | Required - a signed quote obtained off-chain from the Wormhole Executor API | ### Dependencies - **`SwapFacility`** - wraps and unwraps `$M` extensions. The Portal calls `swapOutM()` to unwrap an extension before sending and `swapInM()` to wrap on arrival. - **`MerkleTreeBuilder`** - deployed on Ethereum; builds Merkle roots from registrar lists (such as the Solana earner list) for propagation to SVM chains. - **`MToken` and `Registrar`** - the sources of the earning index and governance values that the hub reads at send time. ## Chain IDs Portal V2 uses a single internal chain ID type (`uint32`) across all contracts and payloads: - On EVM chains, the internal ID **is** the standard EVM chain ID (Ethereum `1`, Base `8453`, Arbitrum One `42161`, and so on). The Portal reads `block.chainid` directly, so a contentious hard fork cannot be used to replay messages. - Non-EVM chains get assigned custom IDs to avoid collisions - Solana is `1399811149`. - Bridge adapters translate internal IDs into provider-specific IDs internally. Callers never handle Wormhole chain IDs, Hyperlane domains, or LayerZero endpoint IDs. The [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol) uses the same internal chain IDs. ## Token Transfers ### Sending Users send tokens by calling `sendToken()` on the Portal contract of the source chain: ```solidity function sendToken( uint256 amount, address sourceToken, // token to debit on this chain uint32 destinationChainId, bytes32 destinationToken, // token to deliver on the destination chain bytes32 recipient, bytes32 refundAddress, // receives any bridge fee overpayment bytes calldata bridgeAdapterArgs ) external payable returns (bytes32 messageId); ``` A second overload adds an explicit `address bridgeAdapter` parameter for callers that want a specific adapter instead of the chain's default. When a transfer is sent: 1. The Portal pulls `sourceToken` from the caller. If it is an extension rather than `$M`, the Portal unwraps it through the `SwapFacility`. 2. On the hub, the resulting `$M` stays locked in the `HubPortal` and the bridged principal for the destination spoke is incremented (isolated spokes only). On a spoke, the `$M` is burned. 3. A `TokenTransfer` payload is encoded with the amount, the current `$M` index, the destination token, and the recipient. 4. The payload is handed to the selected bridge adapter along with the full `msg.value` as the delivery fee. The source and destination token pair must be whitelisted by the operator as a supported bridging path, and the chosen adapter must be registered for the destination chain - otherwise the call reverts with `UnsupportedBridgingPath` or `UnsupportedBridgeAdapter`. ### Receiving On the destination chain, the bridge adapter authenticates the incoming message (provider infrastructure plus a peer check) and calls `receiveMessage()` on its local Portal. Only registered adapters can call it. The Portal then: 1. Checks the message ID against already-processed messages to prevent replays 2. Releases locked `$M` (hub) or mints `$M` (spoke). On spoke chains a transfer carrying a newer index also advances the local index 3. If the destination token is an extension, wraps the `$M` through the `SwapFacility` and delivers it to the recipient ::note **Wrap failure fallback.** If wrapping to the destination extension fails for any reason, the transfer does not revert - the recipient receives plain `$M` instead, and the Portal emits `WrapFailed`. Recipients of bridged extension tokens should be able to handle both the extension and the underlying `$M`. :: ### Bridging Mechanisms - **Hub → Spoke**: `$M` is locked on the hub and minted on the spoke. - **Spoke → Hub**: `$M` is burned on the spoke; the `HubPortal` verifies the amount does not exceed the bridged principal tracked for that spoke, then releases the locked `$M`. - **Spoke → Spoke**: `$M` is burned on the source spoke, minted on the destination spoke - available only between connected spokes. ### Isolated and Connected Spokes Every spoke starts **isolated**: it can only send tokens to and receive tokens from the hub, and the hub tracks its bridged principal so the spoke can never withdraw more than was bridged to it. Isolation applies only to token transfers - fill and cancel reports can be passed between any two supported chains. To connect a spoke, the operator calls `enableCrossSpokeTokenTransfer()` first on the `HubPortal` for that spoke, and then on each `SpokePortal` that should accept the new route (a spoke checks that both its own chain and the counterparty are flagged). ::warning Connecting a spoke is effectively irreversible. Once cross-spoke transfers are enabled, the hub stops tracking bridged principal for that chain - it is reset to zero, since token flows between spokes can no longer be fully accounted for on-chain. :: ## Fees and Quoting Every function that sends a cross-chain message is `payable`; the delivery fee is paid in the native token via `msg.value` and forwarded in full to the bridge adapter. Overpayment is refunded by the underlying messaging protocol to `refundAddress`. Fees are estimated with a single generic quote function, keyed by payload type (see [Message Types](https://docs.m0.org/#message-types)): ```solidity function quote(uint32 destinationChainId, PayloadType payloadType) external view returns (uint256); function quote(uint32 destinationChainId, PayloadType payloadType, address bridgeAdapter) external view returns (uint256); ``` Two provider-specific caveats: - **Wormhole quotes are off-chain.** The Wormhole adapter's `quote()` always reverts with `OnChainQuoteNotSupported` - fees are quoted by the Wormhole Executor API, and the resulting signed quote must be passed as `bridgeAdapterArgs` when sending. - **`refundAddress` must be a valid address on the refunding chain** (the source chain for all currently supported adapters). ## Protocol State Propagation Beyond token transfers, the `HubPortal` propagates protocol state from Ethereum to spoke chains. All of these functions are permissionless - anyone willing to pay the bridge fee can push the latest values. Each comes in two overloads (default adapter, explicit adapter), mirroring `sendToken()`. ### Earning Index `sendMTokenIndex(destinationChainId, refundAddress, bridgeAdapterArgs)` broadcasts the current `$M` earning index so spoke chains accrue yield at the same rate as Ethereum. The index is also embedded in the header of **every** cross-chain message. ### Registrar Data Governance parameters from M0's [TTG governance](https://docs.m0.org/protocol/roles) system are propagated to EVM spoke chains: - **Key-value pairs** via `sendRegistrarKey(destinationChainId, key, ...)` - reads the current value from the Ethereum `Registrar` at send time - **List status** via `sendRegistrarListStatus(destinationChainId, listName, account, ...)` - whether an account is on a governance list, such as the approved earners list ### Earner Merkle Roots (SVM) `sendEarnersMerkleRoot()` sends the Merkle root of the `solana-earners` registrar list to Solana. The Merkle root is built on Ethereum by the `MerkleTreeBuilder` contract. Solana accounts then prove their earner status against the root. Only SVM spokes process this message type. ::note **Message ordering is not guaranteed.** Cross-chain messaging protocols do not guarantee delivery order, so an older registrar value can occasionally arrive after - and overwrite - a newer one. Portal V2 deliberately omits ordering logic; since all state-propagation functions are permissionless, the correct value can simply be re-sent. :: ## OrderBook Integration Portal V2 serves as the messaging layer for the [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol): - **Fill reports** - when a solver fills a cross-chain order on the destination chain, the `OrderBook` calls `sendFillReport()` to relay the fill back to the origin chain, where the Portal delivers it to `reportFill()` on the origin `OrderBook` - **Cancel reports** - `sendCancelReport()` relays destination-chain cancellations back to the origin chain for refund processing Both functions are restricted to the `OrderBook` contract; they cannot be called by users. Unlike token transfers, reports are not subject to spoke isolation and can travel between any two chains. ## Message Types All cross-chain payloads share an 85-byte header, followed by type-specific fields: | Header field | Type | Purpose | | -------------------- | --------- | ----------------------------------------------------- | | Payload type | `uint8` | Identifies the message type (see table below) | | Destination chain ID | `uint32` | Internal chain ID of the target chain | | Destination peer | `bytes32` | Adapter address expected on the destination chain | | Message ID | `bytes32` | Unique ID: `keccak256(sourceChain, destChain, nonce)` | | `$M` index | `uint128` | Current earning index at send time | Carrying the destination chain and peer in every header lets receiving adapters reject messages delivered to the wrong chain or contract - important for protocols like Wormhole where a published message is observable by every chain. | Type | Name | Direction | Payload fields beyond the header | | ---- | ------------------ | -------------------------- | ------------------------------------------------------------------------------- | | 0 | `TokenTransfer` | Hub ↔ Spoke, Spoke ↔ Spoke | `amount`, `destinationToken`, `sender`, `recipient` | | 1 | `Index` | Hub → Spoke | none - the header index is the payload | | 2 | `RegistrarKey` | Hub → Spoke | `key`, `value` | | 3 | `RegistrarList` | Hub → Spoke (EVM) | `listName`, `account`, `add` | | 4 | `FillReport` | Any → Any | `orderId`, `amountInToRelease`, `amountOutFilled`, `originRecipient`, `tokenIn` | | 5 | `EarnerMerkleRoot` | Hub → Spoke (SVM) | `earnerMerkleRoot` | | 6 | `CancelReport` | Any → Any | `orderId`, `orderSender`, `tokenIn`, `amountInToRefund` | Payloads are packed with exact lengths - a message with trailing or missing bytes fails to decode. ## Roles and Permissions The Portal and every bridge adapter are UUPS-upgradeable proxies using OpenZeppelin role-based access control. ### Portal Roles | Role | Capabilities | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | **Admin** (`DEFAULT_ADMIN_ROLE`) | Authorize contract upgrades; grant and revoke all roles | | **Pauser** (`PAUSER_ROLE`) | `pauseSend()`, `pauseReceive()`, `pauseAll()` and the corresponding unpause functions | | **Operator** (`OPERATOR_ROLE`) | `setDefaultBridgeAdapter()`, `setSupportedBridgeAdapter()`, `setSupportedBridgingPath()`, `setPayloadGasLimit()`, `enableCrossSpokeTokenTransfer()` | ### Bridge Adapter Roles | Role | Capabilities | | -------------------------------- | ----------------------------------------------------------------------------------------------------------- | | **Admin** (`DEFAULT_ADMIN_ROLE`) | Authorize adapter upgrades; grant and revoke roles | | **Operator** (`OPERATOR_ROLE`) | `setPeer()`, `setBridgeChainId()`; LayerZero: `setDelegate()`; Wormhole: `setSenderPeer()`, `setMsgValue()` | ### Restricted and Permissionless Calls - Only the **`OrderBook`** contract can call `sendFillReport()` and `sendCancelReport()` - Only registered **bridge adapters** can call `receiveMessage()` on the Portal - `sendToken()`, `sendMTokenIndex()`, `sendRegistrarKey()`, `sendRegistrarListStatus()`, `sendEarnersMerkleRoot()`, `enableEarning()`, and `disableEarning()` are **permissionless** ## Security - **Adapter authentication** - Each adapter accepts inbound messages only from its provider's verified infrastructure (Hyperlane Mailbox, LayerZero Endpoint, valid Wormhole VAAs) and only from the registered peer on the source chain - **Replay protection** - Message IDs are recorded on the receiving Portal and duplicate deliveries revert; the Wormhole adapter additionally tracks consumed VAA hashes - **Wrong-destination protection** - The destination chain ID and adapter address in every payload header are validated on receipt, so a message published for one chain cannot be executed on another - **Independent pause controls** - Sending and receiving can be paused separately, allowing partial operation during incidents - **Per-spoke principal accounting** - For isolated spokes, the `HubPortal` reverts with `InsufficientBridgedBalance` rather than releasing more `$M` than was bridged to that spoke - **Reentrancy protection** - A transient-storage lock guards all send and receive paths - **Wrap failure fallback** - A failing extension wrap on delivery degrades to a plain `$M` transfer instead of blocking the message Audit reports for Portal V2 are listed on the [audits page](https://docs.m0.org/resources/audits). ## Contract Reference The source code for Portal V2 can be found in the [m-portal-v2](https://github.com/m0-foundation/m-portal-v2){rel=""nofollow""} repository. Deployed addresses for every chain are listed in the [deployments reference](https://docs.m0.org/resources/addresses). ### Key Functions | Function | Contract | Description | | ----------------------------------------------- | -------------------------- | -------------------------------------------------------------- | | `sendToken()` | `HubPortal`, `SpokePortal` | Bridge tokens to another chain (2 overloads) | | `quote()` | `HubPortal`, `SpokePortal` | Estimate the bridge fee for a payload type (2 overloads) | | `receiveMessage()` | `HubPortal`, `SpokePortal` | Deliver an inbound message (bridge adapters only) | | `sendMTokenIndex()` | `HubPortal` | Broadcast the earning index (2 overloads) | | `sendRegistrarKey()` | `HubPortal` | Propagate a registrar key-value pair (2 overloads) | | `sendRegistrarListStatus()` | `HubPortal` | Propagate an account's list membership (2 overloads) | | `sendEarnersMerkleRoot()` | `HubPortal` | Broadcast the SVM earner Merkle root (2 overloads) | | `sendFillReport()` / `sendCancelReport()` | `HubPortal`, `SpokePortal` | Relay OrderBook reports (`OrderBook` only, 2 overloads each) | | `enableEarning()` / `disableEarning()` | `HubPortal` | Toggle portal earning (permissionless; disable is permanent) | | `enableCrossSpokeTokenTransfer()` | `HubPortal`, `SpokePortal` | Connect a spoke for spoke-to-spoke transfers (`OPERATOR_ROLE`) | | `pauseSend()` / `pauseReceive()` / `pauseAll()` | `HubPortal`, `SpokePortal` | Pause message flow independently (`PAUSER_ROLE`) | ### View Functions | Function | Description | | --------------------------------------------------------------- | ------------------------------------------------------------- | | `defaultBridgeAdapter(chainId)` | Default adapter configured for a destination chain | | `supportedBridgeAdapter(chainId, adapter)` | Whether an adapter is registered for a destination | | `supportedBridgingPath(sourceToken, chainId, destinationToken)` | Whether a token pair is bridgeable | | `payloadGasLimit(chainId, payloadType)` | Configured destination gas limit per payload type | | `currentChainId()` | This chain's internal (EVM) chain ID | | `currentIndex()` | The `$M` index the Portal would attach to a message right now | | `bridgedPrincipal(spokeChainId)` | Hub only - principal tracked for an isolated spoke | | `crossSpokeTokenTransferEnabled(chainId)` | Whether a spoke is connected | | `hubChainId()` | Spoke only - internal chain ID of the hub | | `mToken()` / `registrar()` / `swapFacility()` / `orderBook()` | Linked protocol contracts | ## Events ```solidity // Token bridging event TokenSent( address indexed sourceToken, uint32 destinationChainId, bytes32 destinationToken, address indexed sender, bytes32 indexed recipient, uint256 amount, uint128 index, address bridgeAdapter, bytes32 messageId ); event TokenReceived( uint32 sourceChainId, address indexed destinationToken, bytes32 indexed sender, address indexed recipient, uint256 amount, uint128 index, bytes32 messageId ); event WrapFailed(address indexed destinationExtension, address indexed recipient, uint256 amount); // State propagation (HubPortal) event MTokenIndexSent(uint32 indexed destinationChainId, uint128 index, address bridgeAdapter, bytes32 messageId); event RegistrarKeySent(uint32 indexed destinationChainId, bytes32 indexed key, bytes32 value, uint128 index, address bridgeAdapter, bytes32 messageId); event RegistrarListStatusSent(uint32 indexed destinationChainId, bytes32 indexed listName, address indexed account, bool status, uint128 index, address bridgeAdapter, bytes32 messageId); event EarnerMerkleRootSent(uint32 indexed destinationChainId, uint128 index, bytes32 earnerMerkleRoot, address bridgeAdapter, bytes32 messageId); event EarningEnabled(uint128 index); event EarningDisabled(uint128 index); // State application (SpokePortal) event MTokenIndexReceived(uint128 index, bytes32 messageId); event RegistrarKeyReceived(bytes32 indexed key, bytes32 value, uint128 index, bytes32 messageId); event RegistrarListUpdateReceived(bytes32 indexed listName, address indexed account, bool add, uint128 index, bytes32 messageId); ``` ## Error Codes | Error | Description | | ---------------------------------------------------- | ------------------------------------------------------------------- | | `UnsupportedBridgingPath` | The source/destination token pair is not whitelisted for that chain | | `UnsupportedBridgeAdapter` | The adapter is not registered for the destination chain | | `PayloadGasLimitNotSet` | No destination gas limit configured for this payload type | | `ZeroAmount` / `ZeroRecipient` / `ZeroRefundAddress` | Required parameter is zero | | `InsufficientAmountReceived` | The Portal received less `$M` than the specified send amount | | `InsufficientBridgedBalance` | Release would exceed the principal bridged to an isolated spoke | | `CrossSpokeTokenTransferDisabled` | Spoke-to-spoke transfer attempted on an isolated route | | `MessageAlreadyProcessed` | Replay attempt - this message ID was already delivered | | `SendingPaused` / `ReceivingPaused` | The corresponding direction is paused | | `NotBridgeAdapter` | `receiveMessage()` called by an unregistered address | | `NotOrderBook` | Fill/cancel report sent by an address other than the `OrderBook` | | `EarningCannotBeReenabled` | Earning was disabled and is permanently off | | `UnsupportedChain` / `UnsupportedSender` | Adapter-level: unknown chain mapping or wrong remote peer | | `OnChainQuoteNotSupported` | Wormhole adapter: use the Executor API for quotes | | `InvalidVaa` | Wormhole adapter: VAA failed Core Bridge verification | ## Related - [Bridging M and wM tokens](https://docs.m0.org/build/bridging-user) - Step-by-step user guide with a block explorer - [Integrating with Portal V2](https://docs.m0.org/build/bridging-developer) - Developer guide with contract calls and a code example - [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol) - The settlement layer that relies on Portal V2 messaging - [Accessing Liquidity](https://docs.m0.org/build/accessing-liquidity) - Overview of M0's liquidity infrastructure - [Deployments](https://docs.m0.org/resources/addresses) - Contract addresses on every supported chain - [Source Code](https://github.com/m0-foundation/m-portal-v2){rel=""nofollow""} - Smart contract repository # PYUSDx Specification This page is the lookup reference for PYUSDx. For explanations of the mechanics, see the [PYUSDx overview](https://docs.m0.org/protocol/pyusdx/overview) and its linked pages. - **Solidity:** 0.8.34 - **EVM target:** Cancun - **License:** BUSL-1.1 --- ## Token parameters | Parameter | Value | | --------------- | -------------------------- | | Name | Configurable (set at init) | | Symbol | Configurable (set at init) | | Decimals | 6 (PYUSD standard) | | Index precision | `1e12` (`EXP_SCALED_ONE`) | | Fee rate max | 10,000 bps (100%) | --- ## Roles Consolidated across all contracts. ### PYUSDx | Role | Holder | Powers | | ------------------------------ | ----------------------- | ------------------------------------------ | | `DEFAULT_ADMIN_ROLE` | Admin | Grant/revoke all roles, set earner manager | | `ISSUER_ROLE` | `IssuerGateway` | `mint`, `burn` | | `PAUSER_ROLE` | Pauser | Pause/unpause all transfers | | `FREEZE_MANAGER_ROLE` | Freeze manager | Freeze/unfreeze accounts | | `FORCED_TRANSFER_MANAGER_ROLE` | Forced transfer manager | Seize funds from frozen accounts | | `RATE_LIMIT_MANAGER_ROLE` | Rate limit manager | Set per-issuer rate limits | The earner manager is a single address, not a role. ### IssuerGateway | Role | Powers | | -------------------- | ---------------------------- | | `DEFAULT_ADMIN_ROLE` | Set mint delay, set mint TTL | | `OPERATOR_ROLE` | Propose mints, burn tokens | | `EXECUTOR_ROLE` | Execute mints after delay | ### Portal | Role | Capabilities | | -------------------- | -------------------------------------------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | Grant/revoke all roles, `setFallbackRecipient` | | `OPERATOR_ROLE` | `setDefaultBridgeAdapter`, `setSupportedBridgeAdapter`, `setPayloadGasLimit` | | `PAUSER_ROLE` | `pauseSend`, `unpauseSend`, `pauseReceive`, `unpauseReceive`, `pauseAll`, `unpauseAll` | ### BridgeAdapter | Role | Capabilities | | -------------------- | -------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | Grant/revoke all roles | | `OPERATOR_ROLE` | `setPeer`, `setBridgeChainId`, `setDelegate` | ### YieldToOne | Role | Powers | | ------------------------------ | -------------------------------- | | `DEFAULT_ADMIN_ROLE` | Grant/revoke roles | | `YIELD_RECIPIENT_MANAGER_ROLE` | Claim yield, set yield recipient | | `VERSION_MANAGER_ROLE` | Pin beacon version | | `FREEZE_MANAGER_ROLE` | Freeze/unfreeze accounts | | `PAUSER_ROLE` | Pause/unpause transfers | ### MultiMint `MultiMint` inherits `YieldToOne` roles and adds `ASSET_CAP_MANAGER_ROLE`, which sets asset caps and manages the `replaceAsset` caller whitelist. --- ## Initialization parameters ### PYUSDx ```solidity struct InitializeParams { string name; string symbol; address admin; address pauser; address freezeManager; address forcedTransferManager; address earnerManager; address rateLimitManager; address issuer; } ``` ### IssuerGateway ```solidity initialize(admin, operator, executor, mintDelay, mintTTL) ``` ### Portal ```solidity initialize(admin, pauser, operator, fallbackRecipient) ``` ### SwapFacility ```solidity initialize(admin, pauser) // Constructor: (pyusdx, extensionFactory) ``` ### ExtensionFactory ```solidity initialize(admin, factoryManager) // Constructor: (pyusdx, swapFacility, yieldToOneBeacon, multiMintBeacon) ``` ### ExtensionBeacon ```solidity initialize(admin, beaconManager, initialImplementation) // Constructor: (pyusdx, swapFacility) ``` --- ## Rounding Rounding is conservative for the protocol. When converting between principal and present amount, PYUSDx never credits more principal than the present amount supports, and never debits less than required. For an earner with current index `I`: - Inbound (`_addEarningAmount`): `principal += floor(amount × 1e12 / I)`, round **down**. - Outbound (`_subtractEarningAmount`): `principal -= ceil(amount × 1e12 / I)`, round **up**. Non-earner accounts have no `earningPrincipal`; their `balance` updates use the exact `amount`. ### PYUSDx methods | Method | Earner | Non-earner | | --------------------------- | ----------------------------------------------------------------- | ------------------- | | `mint` / `distributeReward` | round **down** principal | exact amount | | `burn` | round **up** principal | exact amount | | `transfer` | see transfers below | see transfers below | | `claimFor` | yield = `floor(principal × index / 1e12) − balance`, clamped at 0 | no-op | ### PYUSDx transfers | Sender → Recipient | Sender side → Recipient side | | ----------------------- | ---------------------------- | | earner → earner | round up → round down | | earner → non-earner | round up → amount | | non-earner → earner | amount → round down | | non-earner → non-earner | amount → amount | In all four cases the protocol's net principal accounting is conservative: outgoing principal is over-debited; incoming principal is under-credited. There is no balance or solvency impact; only yield is affected. Moving value through an earning balance can reduce an account's unrealized and future yield by a bounded amount: up to 1 unit (1e-6 PYUSDx) per receive, up to 2 per `wrap()` + `unwrap()` round trip. ### Forced transfer A forced transfer runs on a frozen account, which is always non-earning at seizure (freeze materializes pending yield first). The sender side is therefore exact-amount; the recipient side is earner-aware. | Recipient | Sender (frozen) → Recipient | | ---------- | --------------------------- | | earner | amount → round down | | non-earner | amount → amount | ### Extensions (YieldToOne / MultiMint) Extension tokens are 1:1 wrappers around PYUSDx. The Extension contract itself is a registered earner on PYUSDx, so wrap/unwrap routes PYUSDx into or out of an earner balance via regular `_transfer` calls. | Operation | Net PYUSDx-side rounding | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `wrap` | User → Extension. User-side: round **up** (if earner) or exact (non-earner); Extension-side: round **down**. | | `unwrap` | Extension → User. Extension-side: round **up**; user-side: round **down** (if earner) or exact (non-earner). | | `replaceAsset` (MultiMint) | PYUSDx deposit per the transfer matrix; alternative-asset withdrawal at exact amount (no PYUSDx rounding on the asset leg). | Wrap and unwrap always move PYUSDx **1:1** — the amount of extension tokens minted or PYUSDx returned equals the amount supplied. The rounding above applies only to the Extension's earning **principal** (its future PYUSDx yield), never to the transferred amount or the 1:1 backing. The extension token's own `balanceOf` mapping uses exact-amount accounting; extension tokens are not themselves yield-bearing. ### Portal (cross-chain) Portal moves PYUSDx between chains via burn-and-mint. | Operation | Source-chain rounding | Destination-chain rounding | | ---------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `sendToken` | Sender → Portal per the transfer matrix; Portal (non-earner) burns at exact amount | — | | `receiveMessage` (`destToken == PYUSDX`) | — | Mint to recipient (earner: round **down**; non-earner: exact) | | `receiveMessage` (`destToken` is an Extension) | — | Mint to Portal at exact amount; SwapFacility wraps into the destination Extension | For an earner sender → earner recipient, source-chain principal is debited rounded up while destination-chain principal is credited rounded down. The sub-unit difference is conservative (no over-issuance). --- ## Invariants 1. `totalSupply` equals the sum of all account balances. Unclaimed yield is not included in `totalSupply`; yield enters supply only when materialized by `claimYield`, `setAccountInfo`, or `_beforeFreeze`. 2. An earner's `earningPrincipal` is always ≤ their `balance`. 3. An earner's `lastIndex` is monotonically increasing between the `_setAccountInfo` that enables earning and the `_stopEarningFor` that disables it. `_stopEarningFor` clears `lastIndex` to 0; a subsequent re-enable re-initializes it to `EXP_SCALED_ONE` (`1e12`). `currentIndexOf` returns `EXP_SCALED_ONE` (`1e12`) for non-earners. 4. Freezing an account stops its earning and claims all pending yield first. 5. `totalSupply` can increase during pause only through `setAccountInfo` / `_beforeFreeze` yield materialization. 6. No regular transfers can occur while paused. Additional guarantees: `totalSupply == sum(balanceOf)` at all times (balances are stored directly and always honored), and `accruedYieldOf(account) ≥ 0` (yield is floored at zero). --- ## Cross-chain message format The token-transfer payload is 180 bytes, encoded with `abi.encodePacked`: ```plain ┌──────────────┬──────────────┬────────────┬───────────┬──────────────┬──────────┬─────────────┐ │ Dest ChainID │ Dest Peer │ Message ID │ Amount │ Dest Token │ Sender │ Recipient │ │ (4 bytes) │ (32 bytes) │ (32 bytes) │ (16 bytes)│ (32 bytes) │(32 bytes)│ (32 bytes) │ └──────────────┴──────────────┴────────────┴───────────┴──────────────┴──────────┴─────────────┘ ``` On the destination chain, `Dest ChainID` and `Dest Peer` are read as `targetChainId` and `targetBridgeAdapter` and validated against `currentChainId()` and `msg.sender` respectively. --- ## Errors ### PYUSDx | Error | Condition | | ----------------------------------------------- | -------------------------------------------- | | `ZeroAdmin` | Admin address is zero at init | | `ZeroIssuer` | Issuer address is zero at init | | `ZeroEarnerManager` | Earner manager is zero | | `ZeroAccount` | Account address is zero | | `ZeroAmount` | Amount is zero | | `NotEarnerManager` | Caller is not the earner manager | | `InvalidAccountInfo` | Disabling earning but non-zero fee/recipient | | `FeeRateTooHigh(feeRate)` | Fee rate > 10,000 bps | | `EarnerRateTooHigh(earnerRate)` | Earner rate > 10,000 bps | | `ArrayLengthZero` | Empty array input | | `ArrayLengthMismatch` | Array lengths don't match | | `InsufficientBalance(account, balance, amount)` | Burn exceeds balance | ### IssuerGateway | Error | Condition | | -------------------------------- | --------------------------------- | | `ZeroPYUSDX` | PYUSDx address is zero | | `ZeroMintAmount` | Mint amount is zero | | `ZeroMintRecipient` | Mint recipient is zero | | `ZeroBurnAmount` | Burn amount is zero | | `ZeroMintTTL` | Mint TTL is zero | | `InvalidMintProposal` | Proposal does not exist | | `PendingMintProposal(activeAt)` | Mint not yet active | | `ExpiredMintProposal(expiresAt)` | Mint has expired | | `NotMintProposalCreator` | Caller didn't create the proposal | | `ActiveMintProposal(activeAt)` | Cannot cancel an active proposal | ### RateLimiter | Error | Condition | | ----------------------------------------- | ------------------------------------------------------------------------ | | `ZeroRateLimitManager` | Rate limit manager is zero | | `RateLimitExceeded(requested, available)` | Mint exceeds rate limit | | `InvalidRateLimitRemoval` | Disabling with non-zero params | | `RateLimitNotConfigured(issuer)` | `_enforceRateLimit` invoked for an issuer that has never been configured | | `InvalidRateLimitConfig` | Enabling a rate limit with `capacity == 0` | ### Portal | Error | Condition | | ------------------------------------------------- | --------------------------------------------------------- | | `InvalidTargetChain(targetChainId)` | Decoded `targetChainId` does not match `currentChainId()` | | `InvalidTargetBridgeAdapter(targetBridgeAdapter)` | Decoded `targetBridgeAdapter` does not match `msg.sender` | | `MessageAlreadyProcessed(messageId)` | Inbound `messageId` has already been processed | ### MultiMint | Error | Condition | | -------------------------- | -------------------------------------------------------------- | | `AssetNotAllowed(asset)` | `replaceAsset` called for an asset with `cap == 0` | | `AssetCapReached(asset)` | `wrap` would push asset balance past cap | | `InvalidAsset(asset)` | Asset address is zero, or address mismatch | | `CallerNotAllowed(caller)` | `replaceAsset` caller is not on the enforced whitelist | | `ArrayLengthMismatch` | Batch `setReplaceAssetWhitelistCaller` arrays differ in length | --- ## Events | Contract | Events | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | PYUSDX | `StartedEarning`, `StoppedEarning`, `AccountInfoUpdated`, `EarnerManagerSet`, `IndexUpdated`, `YieldClaimed`, `FeeClaimed`, `RewardDistributed` | | IssuerGateway | `MintProposed`, `MintExecuted`, `MintCanceled`, `BurnExecuted`, `MintDelaySet`, `MintTTLSet` | | Portal | `TokenSent`, `TokenReceived`, `RedirectedToFallbackRecipient`, `WrapFailed`, send/receive pause events | | SwapFacility | `Swapped`, `SwappedIn`, `SwappedOut`, `SwappedInMultiMint`, `MultiMintAssetReplaced` | | YieldToOne | `YieldClaimed`, `YieldRecipientSet`. Pin/unpin emit ERC-1967 `Upgraded` / `BeaconUpgraded` | | MultiMint | `AssetWrapped`, `AssetReplaced`, `AssetCapSet`, `ReplaceAssetWhitelistCallerSet` | --- ## External dependencies | Dependency | Usage | | -------------------------- | ----------------------------------- | | m-extensions | Core shared library | | `ERC20ExtendedUpgradeable` | Base ERC-20 with extensions | | `Freezable` | Account freezing | | `ForcedTransferable` | Fund seizure | | `Pausable` | Global pause | | `ContinuousIndexingMath` | Exponential index math | | `IndexingMath` | Principal/present conversion | | `UIntMath` | Safe uint conversions | | OpenZeppelin Upgradeable | AccessControl, Initializable, Proxy | --- ## Related - [PYUSDx overview](https://docs.m0.org/protocol/pyusdx/overview): what PYUSDx is and its contracts - [Token & yield](https://docs.m0.org/protocol/pyusdx/token-and-yield): the yield model and transfer mechanics - [PYUSDx platform deployments](https://docs.m0.org/resources/addresses/pyusdx-platform): deployed contract addresses # PYUSDx Cross-chain (Portal) ## 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. | Chain | EVM chain ID | | ---------------- | ------------ | | Ethereum | `1` | | Arbitrum | `42161` | | Monad | `143` | | Base | `8453` | | Ethereum Sepolia | `11155111` | | Arbitrum Sepolia | `421614` | | Monad Testnet | `10143` | | Base Sepolia | `84532` | ## 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](https://docs.m0.org/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. | Frozen on PYUSDx | Frozen on Extension | Destination Token | Behavior | | ------------------------------- | ------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- | | No | No | PYUSDx | Mint PYUSDx to recipient | | No | No | Extension | Mint PYUSDx to Portal, wrap to Extension for recipient | | Yes | (either) | PYUSDx | **Redirect:** mint PYUSDx to `fallbackRecipient` | | Yes | (either) | Extension | **Redirect:** mint PYUSDx to Portal, wrap Extension to `fallbackRecipient` | | No | Yes | Extension | Wrap fails → `WrapFailed` → mint PYUSDx directly to recipient | | Yes | (either) | Extension (wrap fails) | **Redirect + WrapFailed:** mint PYUSDx directly to `fallbackRecipient` | | Yes (both recipient & fallback) | — | Any | **Transaction 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`. ::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. :: ## 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](https://docs.m0.org/resources/addresses/pyusdx-platform) page. # PYUSDx Compliance & Security ## Roles Access to PYUSDx operations is governed by role-based access control: | Role | Holder | Powers | | ------------------------------ | ----------------------- | ------------------------------------------ | | `DEFAULT_ADMIN_ROLE` | Admin | Grant/revoke all roles, set earner manager | | `ISSUER_ROLE` | `IssuerGateway` | `mint`, `burn` | | `PAUSER_ROLE` | Pauser | Pause/unpause all transfers | | `FREEZE_MANAGER_ROLE` | Freeze manager | Freeze/unfreeze accounts | | `FORCED_TRANSFER_MANAGER_ROLE` | Forced transfer manager | Seize funds from frozen accounts | | `RATE_LIMIT_MANAGER_ROLE` | Rate limit manager | Set per-issuer rate limits | The earner manager is a single address (not a role) that controls the yield system; see [Token & yield](https://docs.m0.org/protocol/pyusdx/token-and-yield#earner-manager). ### Access-control hierarchy - `DEFAULT_ADMIN_ROLE` can grant or revoke all roles, set the earner manager (PYUSDx), set the fallback recipient (Portal), and set the mint delay/TTL (IssuerGateway). - `ISSUER_ROLE` (held by the `IssuerGateway`) can `mint()` and `burn()` on PYUSDx. - The earner manager can `setAccountInfo()` to configure earners, `distributeReward()` to mint to any account, and receives yield fees. ## Freeze system - Accounts can be frozen and unfrozen by `FREEZE_MANAGER_ROLE`. - Frozen accounts cannot transfer, receive, or claim yield, and cannot be party to an allowance grant: `approve` and EIP-2612 `permit` revert when either the owner or the spender is frozen. - Freezing an earner automatically claims yield and stops earning first. - Forced transfers can seize funds from frozen accounts. ## Forced transfer `_forceTransfer(frozenAccount, recipient, amount)`: - Is only callable by `FORCED_TRANSFER_MANAGER_ROLE`. - Requires `frozenAccount` to be frozen and `recipient` to not be frozen. - Frozen accounts are always non-earning (freeze stops earning first). - Subtracts from the frozen account's balance and adds to the recipient (earner-aware). - The seizable amount is the full `balance`, including any yield that materialized into the account at freeze time via `_beforeFreeze` (see [pause/freeze-time yield behavior](https://docs.m0.org/protocol/pyusdx/token-and-yield#pause-freeze-time-yield-behavior)). ## Pause system - **PYUSDx**: a single global pause blocks all transfers, mints, burns, and claims. `setAccountInfo` remains callable as an emergency lever (with yield materialization to the earner's own balance). - **Portal**: separate send and receive pause states (see [Cross-chain (Portal)](https://docs.m0.org/protocol/pyusdx/bridging#pause-model)). - **SwapFacility / Extensions**: a single global pause per contract. ### Known pause-time behaviors 1. `setAccountInfo` while paused: yield materializes to the earner's balance, and the fee is forfeited. 2. `claimYield` on `YieldToOne`: succeeds while paused, but the minted tokens cannot transfer until unpause. 3. `Portal` send and receive are independently pausable. 4. All standard transfers are blocked during pause. ## Upgradeability All core contracts (PYUSDx, `IssuerGateway`, `Portal`, `SwapFacility`, `ExtensionFactory`, `ExtensionBeacon`, `LayerZeroBridgeAdapter`) sit behind OpenZeppelin's `TransparentUpgradeableProxy`, deployed deterministically through CREATE3. Each proxy has a dedicated `ProxyAdmin` that owns the upgrade entry point; calls from the admin are routed to the proxy's admin surface, and all other calls fall through to the implementation. - Implementation contracts call `_disableInitializers()` in the constructor. - Initializer functions use the `initializer` or `onlyInitializing` modifiers and explicitly chain every inherited `__X_init` initializer (for example `__AccessControl_init`), so no parent initialization is skipped. - Storage uses ERC-7201 namespaced locations to avoid collisions across upgrades. Extensions (`YieldToOne`, `MultiMint`) use the beacon proxy pattern instead. Each extension type has its own beacon, so a beacon upgrade atomically rolls all instances of that type forward (subject to per-extension version pinning; see [Extensions](https://docs.m0.org/protocol/pyusdx/extensions#beacon-upgrade-and-pinning)). ## Reentrancy protection Two mechanisms protect against reentrancy: 1. **ReentrancyLock** (transient storage): used by `Portal` and `SwapFacility`. It stores `msg.sender` as the lock, enabling caller resolution through the guard. 2. **Checks-Effects-Interactions**: all state changes occur before external calls. ## Cross-chain security - Message IDs prevent replay attacks across chains: each `messageId` can be processed at most once on the destination chain. - Bridge adapters must be explicitly registered per destination chain. - The fallback recipient prevents frozen-account reverts from stranding bridged funds. - Failed wraps gracefully degrade to a direct PYUSDx transfer. Deployed contract addresses are listed on the [PYUSDx platform deployments](https://docs.m0.org/resources/addresses/pyusdx-platform) page. # PYUSDx Extensions Extensions are branded ERC-20 tokens backed 1:1 by PYUSDx. They inherit freeze/pause controls and route the underlying PYUSDx yield to designated recipients. Two extension types exist: `YieldToOne` and `MultiMint`. ## SwapFacility The `SwapFacility` enables 1:1 swaps between PYUSDx and approved extension tokens. It is the exclusive gateway for wrapping and unwrapping. ### Swap paths | From | To | Mechanism | | -------------- | ----------- | -------------------------------------------------------------------------------- | | PYUSDx | Extension | `swapIn`: transfer PYUSDx in, `Extension.wrap()` mints extension tokens | | Extension | PYUSDx | `swapOut`: `Extension.unwrap()` burns extension tokens, transfer PYUSDx out | | Extension A | Extension B | `swapExtensions`: unwrap A → wrap B | | Asset (ERC-20) | MultiMint | `swapInMultiMint`: deposit asset, `MultiMint.wrap(asset)` mints extension tokens | | Asset + PYUSDx | MultiMint | `replaceAsset`: deposit PYUSDx/extension, extract asset from MultiMint reserves | ### Key constraints - Self-swaps are forbidden (`tokenIn == tokenOut` reverts). - Only approved extensions (registered in `ExtensionFactory`) can be swapped. - Fee-on-transfer tokens are not supported: the actual amount received must equal the specified amount. - `swapInMultiMint` of an asset with more than 6 decimals: `MultiMint` pulls only the largest non-dust multiple, and `SwapFacility` refunds any leftover dust to the caller; the emitted `SwappedInMultiMint` amount reflects the net deposited. - Permit-based approvals are supported for gasless swaps. `SwapFacility` uses the same `ReentrancyLock` as `Portal`, which enables `msgSender()` resolution through the lock. This is critical because extensions call back to `SwapFacility.msgSender()` to identify the original caller. ## Extension system The extension system deploys extensions as beacon proxies. It consists of: 1. **ExtensionBeacon**: a single-type, ERC-1967-compliant versioned implementation registry (one beacon instance per extension type). 2. **ExtensionFactory**: deploys and registers extensions via CREATE3. 3. **ExtensionBeaconProxy**: a per-extension proxy supporting beacon mode (follow-latest) and pinned mode. 4. **Extension** (abstract): the base contract with wrap/unwrap/pause/freeze. The `ExtensionType` enum distinguishes the types: `NONE` (0, not registered), `YIELD_TO_ONE` (1), and `MULTI_MINT` (2). ### Deployment flow A deployer deploys extensions via the `ExtensionFactory` by calling either `deployYieldToOne(extensionName, params)` or `deployMultiMint(extensionName, params)`. The factory then: 1. Resolves the latest implementation from the extension type's `ExtensionBeacon`. 2. Encodes the extension's initializer calldata. 3. Deploys the `ExtensionBeaconProxy` via CREATE3 (deterministic address). 4. Registers the proxy in the `ExtensionFactory`'s extension type mapping. 5. Emits `ExtensionDeployed()`. `ExtensionFactory.registerExtension(extension, type)` (gated by `FACTORY_MANAGER_ROLE`) registers or unregisters an extension manually. A registered extension must first be unregistered (type set to `NONE`) before being re-registered under a different type. Re-registering a still-registered extension reverts with `ExtensionAlreadyRegistered`. The CREATE3 salt is the `abi.encodePacked` of `bytes20(extensionFactoryAddress) + bytes1(0) + bytes11(keccak256(deployerAddress, extensionName))`. A deployer can only deploy one extension per network using the same `extensionName`, which lets the deployer deterministically deploy their extension at the same address across several EVM networks. ::note{title="extensionName vs name"} The `extensionName` parameter is used only to compute the salt and deploy the `ExtensionBeaconProxy` at a deterministic address; it is **not** the ERC-20 `name` of the extension. The `name` parameter sets the extension's token name. :: ### Beacon upgrade and pinning Each extension type has its **own** `ExtensionBeacon` instance (a `YieldToOne` beacon and a `MultiMint` beacon), each a single-type ERC-1967 implementation registry. 1. **BeaconManager** calls `registerImplementation(implementation)` on a beacon. The version auto-increments (starting at 1) and the beacon emits the ERC-1967 `Upgraded(implementation)` event. 2. All proxies served by that beacon upgrade atomically when its `latestVersion` changes, unless an individual proxy is pinned. An `ExtensionBeaconProxy` operates in one of two ERC-1967 modes: - **Beacon mode** (default): the `eip1967.proxy.beacon` slot holds the beacon address; the proxy resolves the beacon's latest implementation on every call and auto-upgrades. - **Pinned mode**: the `eip1967.proxy.implementation` slot holds a frozen implementation address; the proxy ignores the beacon and never auto-upgrades. Switching modes mutates the `eip1967` slots so external ERC-1967 readers (block explorers, tooling) correctly detect a beacon proxy versus a direct proxy. The origin beacon address is additionally stored in a dedicated slot, written once at construction and never modified, so a pinned proxy can always be un-pinned back to its beacon. The extension's `VERSION_MANAGER_ROLE` controls pinning: - `pinVersion(version)`: pins to a specific beacon version. `version` must be `> 0` (reverts with `ZeroVersion`). Resolves the implementation from the origin beacon, switches the proxy to pinned mode, and emits `Upgraded`. - `unpinVersion()`: restores beacon mode (follow-latest) and emits `BeaconUpgraded`. Reverts with `NotPinned` if the proxy is not currently pinned. Views: `isPinned()`, `pinnedImplementation()`, `originBeacon()`. ### Extension base contract The abstract `Extension` contract provides: - `wrap(recipient, amount)`: called by `SwapFacility` only. Pulls PYUSDx, mints extension tokens. - `unwrap(amount)`: called by `SwapFacility` only. Burns extension tokens, returns PYUSDx. - Freeze/pause hooks: `_beforeTransfer`, `_beforeWrap`, `_beforeUnwrap`, `_beforeApprove`. - Immutable references: `pyusdx`, `swapFacility`. ## YieldToOne `YieldToOne` is a 1:1 PYUSDx wrapper where all yield accrues to a single `yieldRecipient`. Holders get a stable, non-rebasing token while the yield recipient captures the PYUSDx yield earned on the extension's aggregate balance. ### Yield flow 1. PYUSDx tokens are wrapped into `YieldToOne` via `SwapFacility`. 2. The `YieldToOne` contract itself is registered as an earner on PYUSDx (by the earner manager). 3. PYUSDx yield accrues on the contract's PYUSDx balance. 4. `claimYield()` (called by `YIELD_RECIPIENT_MANAGER_ROLE`): - Realizes pending PYUSDx yield via `IPYUSDX.claimFor(address(this))`. - Computes `excess = PYUSDX_balance - totalSupply` (which also accounts for direct PYUSDx donations). - Mints `excess` extension tokens to `yieldRecipient`. ### Incident response - `claimYield` succeeds even while paused (tokens are minted but cannot transfer until unpause). - `setYieldRecipient` skips the claim if the outgoing recipient is frozen, avoiding a revert that would block rotation. - Yield on PYUSDx can be redirected via `setAccountInfo` at the PYUSDx level. ### Roles | Role | Powers | | ------------------------------ | -------------------------------- | | `DEFAULT_ADMIN_ROLE` | Grant/revoke roles | | `YIELD_RECIPIENT_MANAGER_ROLE` | Claim yield, set yield recipient | | `VERSION_MANAGER_ROLE` | Pin beacon version | | `FREEZE_MANAGER_ROLE` | Freeze/unfreeze accounts | | `PAUSER_ROLE` | Pause/unpause transfers | ## MultiMint `MultiMint` extends `YieldToOne` with **multi-collateral backing**. Users can mint extension tokens by depositing PYUSDx or approved alternative stablecoins. Unwrapping always returns PYUSDx; alternative assets can only be extracted via `replaceAsset`. ### Asset management - `setAssetCap(asset, cap)`: sets the maximum deposit for an asset. `cap > 0` enables the asset for both `wrap` and `replaceAsset`. Setting `cap == 0` is the canonical disable lever and blocks both `wrap` and `replaceAsset` for the asset; existing balances remain unwrappable to PYUSDx (held reserves are not stranded). - `wrap(asset, recipient, amount)`: deposits `asset` tokens and mints extension tokens, respecting the cap. - `replaceAsset(asset, recipient, amount)`: deposits PYUSDx and extracts `asset` from reserves. Reverts with `AssetNotAllowed(asset)` if the asset has `cap == 0`, and with `CallerNotAllowed(caller)` if the replaceAsset whitelist is enforced and the caller is not on it. - Unwrap always returns PYUSDx (inherited from `YieldToOne`). ### Decimal conversion Extension tokens use 6 decimals (matching PYUSDx). When wrapping assets with different decimals, `_fromAssetToExtensionAmount` and `_fromExtensionToAssetAmount` convert between the two. For assets with more than 6 decimals, the deposit on `wrap` is rounded down to the largest multiple of the decimal factor that maps cleanly to extension units. The truncating-division dust is left with the depositor (and refunded by `SwapFacility` on `swapInMultiMint`), keeping `assetBalance == totalAssets × factor` exact across repeated wraps. ### Backing model [[[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[t]{.mord.mathnormal}[o]{.mord.mathnormal}[t]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[S]{.mord.mathnormal style="margin-right:0.0576em;"}[u]{.mord.mathnormal}[ppl]{.mord.mathnormal style="margin-right:0.0197em;"}[y]{.mord.mathnormal style="margin-right:0.0359em;"}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1.0044em;vertical-align:-0.31em;"}[P]{.mord.mathnormal style="margin-right:0.1389em;"}[Y]{.mord.mathnormal style="margin-right:0.2222em;"}[U]{.mord.mathnormal style="margin-right:0.109em;"}[S]{.mord.mathnormal style="margin-right:0.0576em;"}[D]{.mord.mathnormal style="margin-right:0.0278em;"}[X]{.mord.mathnormal style="margin-right:0.0785em;"}[\_]{.mord style="margin-right:0.0278em;"}[ba]{.mord.mathnormal}[c]{.mord.mathnormal}[k]{.mord.mathnormal style="margin-right:0.0315em;"}[in]{.mord.mathnormal}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[t]{.mord.mathnormal}[o]{.mord.mathnormal}[t]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[A]{.mord.mathnormal}[sse]{.mord.mathnormal}[t]{.mord.mathnormal}[s]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} - `PYUSDX_backing = totalSupply - totalAssets` is the PYUSDx actually held. - `excess = PYUSDX_balance - PYUSDX_backing` is available for yield claiming. ### replaceAsset caller whitelist `replaceAsset` (and its `SwapFacility` entry points) is gated by an optional caller whitelist: - The whitelist is a set of addresses managed by `ASSET_CAP_MANAGER_ROLE` via `setReplaceAssetWhitelistCaller(caller, allowed)` and its batch variant (which reverts with `ArrayLengthMismatch` on uneven arrays). - **Disabled when empty**: if the whitelist holds no addresses, `replaceAsset` is open to any caller. Adding the first address switches the whitelist to *enforced*; `isReplaceAssetWhitelistEnabled()` reflects this state. - **Enforced when non-empty**: `replaceAsset` reverts with `CallerNotAllowed(caller)` for any caller not on the list. The caller is the original `msgSender()` resolved through `SwapFacility`'s reentrancy lock, not the `SwapFacility` itself. - `getReplaceAssetWhitelist()` returns the current set; `isAllowedToReplaceAsset(caller, asset, amount)` folds the whitelist check into its result. Deployed contract addresses are listed on the [PYUSDx platform deployments](https://docs.m0.org/resources/addresses/pyusdx-platform) page. # PYUSDx Issuance (IssuerGateway) ## Purpose Every mint of PYUSDx flows through the `IssuerGateway`. It enforces a **time delay** and a **rate limit** on mints, creating a security buffer that gives reviewers a window to catch a bad proposal before it executes. ::note The `Portal` can also mint and burn PYUSDx, but it is not able to issue new PYUSDx under normal operation. See [Cross-chain (Portal)](https://docs.m0.org/protocol/pyusdx/bridging). :: ## Mint lifecycle Mints move through a propose → delay → execute flow: ::steps ### Propose An account with `OPERATOR_ROLE` calls `proposeMint(amount, recipient)`, which emits `MintProposed(mintId, ..., activeAt, expiresAt)`. The proposal's `activeAt` and `expiresAt` timestamps are snapshotted at this point. The original proposer can call `cancelMint(mintId)` any time **before** `activeAt`. ### Execute An account with `EXECUTOR_ROLE` calls `mint(mintId)` between `activeAt` and `expiresAt`. If the proposal is not executed before `expiresAt`, it becomes expired and can no longer be minted. :: ::note{title="Delay and TTL changes do not affect in-flight proposals"} `activeAt` and `expiresAt` are snapshotted at `proposeMint` time and read from storage in `mint` / `cancelMint`. Changes to the global `mintDelay` or `mintTTL` apply only to proposals created **after** the update. :: ### OPERATOR\_ROLE revalidation At execution, `mint` re-checks that the original proposer (`proposal.minter`) still holds `OPERATOR_ROLE`. If the role was revoked after the proposal was created, `mint` reverts with `AccessControlUnauthorizedAccount`. A proposer who loses operator privileges cannot have their pending mints executed. ## Burn lifecycle Burns have no delay. An account with `OPERATOR_ROLE` calls `burn(amount)` directly. The tokens are burned from the caller's own balance (`burn(msg.sender, amount)`), so the operator must hold at least `amount` of PYUSDx for the burn to succeed. ## Roles | Role | Powers | | -------------------- | ----------------------------- | | `DEFAULT_ADMIN_ROLE` | Set mint delay, set mint TTL | | `OPERATOR_ROLE` | Propose mints, burn tokens | | `EXECUTOR_ROLE` | Execute mints after the delay | ## Immutable reference The `pyusdx` address is set in the constructor and cannot be changed. Deployed contract addresses are listed on the [PYUSDx platform deployments](https://docs.m0.org/resources/addresses/pyusdx-platform) page. # PYUSDx Overview PYUSDx is an upgradeable, non-rebasing ERC-20 token with per-account claimable yield, built for the M0 ecosystem. Beyond standard ERC-20 transfers, it supports a multi-asset extension system: PYUSDx can be wrapped 1:1 into branded extension tokens (`YieldToOne`, `MultiMint`) that inherit its freeze and pause controls and route yield to designated recipients. This documentation is for developers and integrators building on PYUSDx. It covers how yield accrues and is claimed per account, how tokens are minted and burned, how the extension system wraps PYUSDx into branded tokens, and how PYUSDx moves across chains. It assumes familiarity with ERC-20 tokens and Solidity smart contracts. ## Overview PYUSDx has **6 decimals** (PYUSD standard) and extends standard ERC-20 semantics with five capabilities: - **Continuous per-earner yield indexing**: each account accrues yield independently at a configurable rate, backed by continuous compounding math. - **Multi-asset extension system**: PYUSDx can be wrapped into branded extension tokens (`YieldToOne`, `MultiMint`) that inherit freeze/pause controls and route yield to designated recipients. - **Cross-chain bridging**: a `Portal` contract enables trustless token transfers across EVM chains via pluggable bridge adapters. - **Compliance primitives**: per-account freezing, forced transfers, and global pausing are first-class operations. - **Time-delayed minting**: mint proposals go through a configurable delay before execution, providing a security buffer. All contracts use the ERC-7201 namespaced storage pattern for upgradeability-safe storage layout. ## Contracts | Contract | Description | | ---------------------- | ------------------------------------------------------------------------------------------------ | | `PYUSDX` | Core ERC-20 token with per-earner yield | | `IssuerGateway` | Time-delayed mint gateway | | `Portal` | Cross-chain token bridge router | | `SwapFacility` | PYUSDx ↔︎ Extension swap router | | `Extension` | Base ERC-20 extension contract | | `YieldToOne` | 1:1 wrapped PYUSDx, yield to single recipient | | `MultiMint` | Multi-collateral wrapped PYUSDx. Inherits from `YieldToOne` capabilities. | | `ExtensionFactory` | Deploys and registers extensions | | `ExtensionBeacon` | Per-type versioned implementation registry (one instance per extension type, ERC-1967 compliant) | | `ExtensionBeaconProxy` | Beacon proxy with pinned versioning | | `BridgeAdapter` | Base for chain-specific bridge adapters | | `RateLimiter` | Token-bucket rate limiting. Inherited by `PYUSDX`. | | `ReentrancyLock` | Transient-storage reentrancy guard. Inherited by `Portal` and `SwapFacility`. | Deployed contract addresses are listed on the [PYUSDx platform deployments](https://docs.m0.org/resources/addresses/pyusdx-platform) page. ## Explore the documentation ::card-group :::card --- icon: i-lucide-coins title: Token & yield to: https://docs.m0.org/protocol/pyusdx/token-and-yield --- Token parameters, the per-account yield model, transfers, mint/burn, and rate limiting. ::: :::card --- icon: i-lucide-file-clock title: Issuance (IssuerGateway) to: https://docs.m0.org/protocol/pyusdx/issuance --- Time-delayed mint proposals, execution, and burns. ::: :::card --- icon: i-lucide-layers title: Extensions to: https://docs.m0.org/protocol/pyusdx/extensions --- The SwapFacility, extension factory and beacons, YieldToOne, and MultiMint. ::: :::card --- icon: i-lucide-arrow-left-right title: Cross-chain (Portal) to: https://docs.m0.org/protocol/pyusdx/bridging --- Burn-and-mint bridging across EVM chains via pluggable adapters. ::: :::card --- icon: i-lucide-shield-check title: Compliance & security to: https://docs.m0.org/protocol/pyusdx/compliance-and-security --- Roles, freezing, forced transfers, pausing, and upgradeability. ::: :::card --- icon: i-lucide-file-text title: PYUSDx specification to: https://docs.m0.org/protocol/pyusdx-spec --- Reference tables: parameters, roles, rounding matrices, errors, and events. ::: :: # PYUSDx Token & Yield ## Token parameters PYUSDx is an upgradeable, non-rebasing ERC-20 token. Its `name` and `symbol` are configurable and set at initialization. It uses **6 decimals** (PYUSD standard). The yield index uses a precision of `1e12` (`EXP_SCALED_ONE`), and fee and earner rates are expressed in basis points with a maximum of 10,000 bps (100%). ## Earner manager The earner manager is a single address (not a role) that controls the yield system. It can: - `setAccountInfo()`: enable, disable, or modify an account's earner configuration. - `distributeReward()`: mint additional tokens to any account. The earner manager also receives fees from yield claims. It is set at initialization and can be changed by the `DEFAULT_ADMIN_ROLE`. ## Yield model PYUSDx implements **non-rebasing, per-account claimable yield** via continuous compounding. Unlike a rebasing token, an account's stored `balance` does not grow on its own. Yield accrues in the background and is only added to the balance when it is claimed (or otherwise materialized). ### How yield accrues 1. The earner manager calls `setAccountInfo(account, earnerRate, feeRate, claimRecipient)` to enable earning. 2. The account's `earningPrincipal` is derived from its current balance. 3. Each account maintains a **personal** `lastIndex` that compounds continuously: [[[]{.katex-mathml}[[[]{.strut style="height:0.6944em;"}[c]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[a]{.mord.mathnormal}[s]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:2.446em;vertical-align:-0.996em;"}[exp]{.mop}[]{.mspace style="margin-right:0.1667em;"}[[[(]{.delimsizing.size3}]{.mopen.delimcenter style="top:0em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[S]{.mord.mathnormal style="margin-right:0.0576em;"}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[C]{.mord.mathnormal style="margin-right:0.0715em;"}[O]{.mord.mathnormal style="margin-right:0.0278em;"}[N]{.mord.mathnormal style="margin-right:0.109em;"}[D]{.mord.mathnormal style="margin-right:0.0278em;"}[S]{.mord.mathnormal style="margin-right:0.0576em;"}[\_]{.mord style="margin-right:0.0278em;"}[P]{.mord.mathnormal style="margin-right:0.1389em;"}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[\_]{.mord style="margin-right:0.0278em;"}[Y]{.mord.mathnormal style="margin-right:0.2222em;"}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[A]{.mord.mathnormal}[R]{.mord.mathnormal style="margin-right:0.0077em;"}]{.mord}]{style="top:-2.314em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[e]{.mord.mathnormal}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[e]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[a]{.mord.mathnormal}[p]{.mord.mathnormal}[se]{.mord.mathnormal}[d]{.mord.mathnormal}[T]{.mord.mathnormal style="margin-right:0.1389em;"}[im]{.mord.mathnormal}[e]{.mord.mathnormal}]{.mord}]{style="top:-3.677em;"}]{.vlist style="height:1.3714em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.996em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[[)]{.delimsizing.size3}]{.mclose.delimcenter style="top:0em;"}]{.minner}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}:br where `earnerRate` is the annual rate converted from basis points (via `convertFromBasisPoints`), `elapsedTime` is the seconds since the last update, and `SECONDS_PER_YEAR` is `31,536,000`. Dividing by `SECONDS_PER_YEAR` expresses the elapsed time as a fraction of a year. 4. The `earningPrincipal` stays fixed while the present value grows as the index grows: [[[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[ese]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[V]{.mord.mathnormal style="margin-right:0.2222em;"}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[u]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.3674em;vertical-align:-0.996em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[E]{.mord.mathnormal style="margin-right:0.0576em;"}[X]{.mord.mathnormal style="margin-right:0.0785em;"}[P]{.mord.mathnormal style="margin-right:0.1389em;"}[\_]{.mord style="margin-right:0.0278em;"}[S]{.mord.mathnormal style="margin-right:0.0576em;"}[C]{.mord.mathnormal style="margin-right:0.0715em;"}[A]{.mord.mathnormal}[L]{.mord.mathnormal}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[D]{.mord.mathnormal style="margin-right:0.0278em;"}[\_]{.mord style="margin-right:0.0278em;"}[O]{.mord.mathnormal style="margin-right:0.0278em;"}[N]{.mord.mathnormal style="margin-right:0.109em;"}[E]{.mord.mathnormal style="margin-right:0.0576em;"}]{.mord}]{style="top:-2.314em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[c]{.mord.mathnormal}[i]{.mord.mathnormal}[p]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[c]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.mord}]{style="top:-3.677em;"}]{.vlist style="height:1.3714em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.996em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} 5. Yield is the difference between the compounded value and the stored balance: `presentValue - balance`. Each account carries its own index, so earners accrue independently. ### Index math - `ContinuousIndexingMath` (from m-extensions) provides the `exp()` approximation. - `IndexingMath` handles principal ↔︎ present-amount conversions. - `UIntMath.bound128()` caps index growth to `uint128.max`. - Earner rates are in basis points (0–10,000), converted via `convertFromBasisPoints`. ### Claiming yield Anyone can call `claimFor(account)`. It mints the account's accrued yield (`yieldWithFee = presentValue - balance`) as new tokens (increasing `totalSupply`), then splits it two ways: - **Fee** (`yieldWithFee × feeRate / 10000`) → the earner manager. - **Net yield** (`yieldNetOfFee = yieldWithFee - fee`) → the account's `claimRecipient`. If the `claimRecipient` is the account itself, the yield simply stays in its balance, and no transfer occurs. Special cases: - `skipTransfer=true` (used by freeze and pause-time `setAccountInfo`): yield materializes to the earner's own balance; routing and fee transfers are skipped. - Non-earners: `claimFor` is a no-op. Whenever yield is materialized, `claimFor` refreshes the account's `lastIndex` and `lastUpdateTimestamp` to the current index on every path (including a zero-fee self-claim where no recipient transfer occurs), so subsequent yield always accrues from the moment of the claim. ### Pause/freeze-time yield behavior Yield can also materialize via the `skipTransfer=true` path (no fee or claim-recipient routing) in two scenarios outside a normal `claimFor`: - **While paused**: `claimFor()` reverts (it is `whenNotPaused`). `setAccountInfo()` remains callable as an emergency lever and materializes yield to the earner's own balance. - **At freeze**: `_beforeFreeze()` uses `skipTransfer=true`, so yield materializes before freezing. Once materialized, the yield is part of the account's regular `balance` and can subsequently be seized by a [forced transfer](https://docs.m0.org/protocol/pyusdx/compliance-and-security#forced-transfer). ### Overflow safety - `_addEarningAmount` reverts when `earningPrincipal` would exceed `uint112`. - `_getPrincipalAmountRoundedDown` and `_getPrincipalAmountRoundedUp` use `UIntMath.safe240()` for the principal cast; inputs above `type(uint240).max` revert with `InvalidUInt240` instead of truncating. ## Transfer mechanics `_transfer(sender, recipient, amount)` behaves as follows: 1. Reverts if paused, or if the sender, recipient, or `msg.sender` is frozen. 2. If the sender is an earner: subtracts from both `balance` and `earningPrincipal` (principal rounded **up**). 3. If the sender is a non-earner: subtracts from `balance` only. 4. If the recipient is an earner: adds to both `balance` and `earningPrincipal` (principal rounded **down**). 5. If the recipient is a non-earner: adds to `balance` only. 6. Earners have their index snapshotted (`_updateIndexOf`) on every balance change. ::note{title="Earner rounding"} Because incoming principal is rounded down and outgoing principal is rounded up, moving value through an earning balance is slightly lossy in **yield** terms, never in balance or backing. A vault-style integrator that mints shares against `balanceOf + accruedYieldOf` should call `claimFor(self)` first, since a small incoming transfer can leave `accruedYieldOf` decreased by up to one unit. The exact per-operation rounding matrices are on the [PYUSDx specification](https://docs.m0.org/protocol/pyusdx-spec#rounding) page. :: ## Mint and burn - **Mint** (`_mint`): only `ISSUER_ROLE`. Rate-limited. Adds to `balance` (and `earningPrincipal` if the account is an earner). Increases `totalSupply`. - **Burn** (`burn`): only `ISSUER_ROLE`. The account must not be frozen. Subtracts from `balance` (and `earningPrincipal` if an earner). Decreases `totalSupply`. Burned tokens are sent to `address(0)`. - **Distribute reward** (`distributeReward`): only the earner manager. Mints new tokens to an account (also rate-limited). Emits `RewardDistributed`. ::note{title="Who ISSUER_ROLE burns from"} The two `ISSUER_ROLE` holders only ever burn their **own** tokens: `Portal` burns `address(this)`, and `IssuerGateway` burns the operator that called it (`burn(msg.sender, amount)`). No path lets an issuer burn a third party's balance. `burn` handles both account types: for a non-earner it reduces `balance` only, and for an earner it also reduces `earningPrincipal`, with the consumed principal rounded **up** (in the protocol's favor). As a result, any unclaimed yield at the moment of burn absorbs a sub-unit rounding loss in present value. :: ## Rate limiting Mints and reward distributions are governed by a per-issuer token-bucket rate limit: - `capacity`: the maximum burst size. - `refillPerSecond`: the refill rate. Rate-limit configuration is **mandatory**: `_enforceRateLimit` reverts with `RateLimitNotConfigured(issuer)` when called for an issuer that has never been configured, and `getRateLimitConfig` / `getRemainingAmount` return `(0, 0)` and `0` respectively for unconfigured issuers. Enabling a rate limit with `capacity == 0` reverts with `InvalidRateLimitConfig` to prevent a "configured but always reverts" bucket state. The limit is enforced on every `mint` and `distributeReward` call. ## Key invariants 1. `totalSupply` equals the sum of all account balances. Unclaimed yield is **not** included in `totalSupply`; yield enters supply only when it is materialized by `claimYield`, `setAccountInfo`, or `_beforeFreeze` (each mints `yieldWithFee` into the account's balance and increments `totalSupply` by the same amount). 2. An earner's `earningPrincipal` is always ≤ their `balance`. 3. An earner's `lastIndex` is monotonically increasing between the `_setAccountInfo` call that enables earning and the `_stopEarningFor` call that disables it. `_stopEarningFor` clears `lastIndex` to 0; a subsequent `_setAccountInfo` that re-enables earning re-initializes `lastIndex` to `EXP_SCALED_ONE` (`1e12`). `currentIndexOf` returns `EXP_SCALED_ONE` (`1e12`) for non-earners. 4. Freezing an account stops its earning and claims all pending yield first. 5. `totalSupply` can increase during pause only through `setAccountInfo` / `_beforeFreeze` yield materialization. 6. No regular transfers can occur while paused. The exact rounding matrices behind these invariants are on the [PYUSDx specification](https://docs.m0.org/protocol/pyusdx-spec#rounding) page. # Rate Models & Yield ## The Interest Mechanism `$M` token operates within a balanced interest system in the M0 Protocol: 1. **Minter Interest Accrual**: Minters accrue obligations at the minter rate on their outstanding `$M` balances. 2. **Earner Interest Accrual**: Simultaneously, approved earning accounts accrue additional tokens at the earner rate. 3. **Rate Synchronization**: The EarnerRateModel ensures the earner rate is calibrated so total earnings never exceed total minter obligations (see Rate Model section below for further details). 4. **Continuous Mechanism**: Both sides update through independent but synchronized continuous indexing systems. This creates a mathematically balanced system where earner yields are safely constrained by the protocol's income from minters. ![Virtual Flow](https://docs.m0.org/images/technical-documentations/m/virtual-flow.png) `$M` token implements sophisticated interest rate models that ensure financial soundness while distributing yield between minters, earners, and the protocol. ## Rate Model Architecture The protocol uses two primary rate models: 1. **MinterRateModel**: Determines the interest rate minters pay on borrowed `$M`token - Simple model that reads rates directly from the TTG Registrar - Capped at 400% APR (40,000 basis points) for system safety - Governance-controlled through the `base_minter_rate` parameter 2. **EarnerRateModel**: Calculates the interest rate paid to `$M`token holders who opt in to earning - Uses a mathematical formula that ensures financial soundness - Rates are capped at 98% of the calculated safe rate - Governed through the `max_earner_rate` parameter ### Minter Rate Calculation The `MinterRateModel` has a straightforward implementation: [[[]{.katex-mathml}[[[]{.strut style="height:0.6833em;"}[min]{.mord.mathnormal}[t]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[min]{.mord.mathnormal}[(]{.mopen}[[ TTGRegistrar(]{.mord}[[[BASE-MINTER-RATE]{.mord}]{.mord.text}]{.mord}[)]{.mord}]{.mord.text}[,]{.mpunct}[]{.mspace style="margin-right:0.1667em;"}[[MAX-MINTER-RATE]{.mord}]{.mord.text}[)]{.mclose}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} ```solidity function rate() external view returns (uint256) { return min(TTGRegistrar.get("base_minter_rate"), MAX_MINTER_RATE); } ``` - The rate is governance-controlled through the TTG Registrar - Has a hard cap of 400% APR to prevent system abuse - Changes to this rate affect all minters uniformly ### Earner Rate Calculation The EarnerRateModel employs a more complex approach to ensure system solvency: [[[]{.katex-mathml}[[[]{.strut style="height:0.6833em;"}[e]{.mord.mathnormal}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[min]{.mord.mathnormal}[(]{.mopen}[ma]{.mord.mathnormal}[x]{.mord.mathnormal}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[,]{.mpunct}[]{.mspace style="margin-right:0.1667em;"}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[e]{.mord.mathnormal}[tE]{.mord.mathnormal style="margin-right:0.0576em;"}[x]{.mord.mathnormal}[t]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[a]{.mord.mathnormal}[S]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[f]{.mord.mathnormal style="margin-right:0.1076em;"}[e]{.mord.mathnormal}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[)]{.mclose}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}[[[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[e]{.mord.mathnormal}[tE]{.mord.mathnormal style="margin-right:0.0576em;"}[x]{.mord.mathnormal}[t]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[a]{.mord.mathnormal}[S]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[f]{.mord.mathnormal style="margin-right:0.1076em;"}[e]{.mord.mathnormal}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[s]{.mord.mathnormal}[a]{.mord.mathnormal}[f]{.mord.mathnormal style="margin-right:0.1076em;"}[e]{.mord.mathnormal}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[M]{.mord.mathnormal style="margin-right:0.109em;"}[u]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[t]{.mord.mathnormal}[i]{.mord.mathnormal}[pl]{.mord.mathnormal style="margin-right:0.0197em;"}[i]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} ```solidity function rate() external view returns (uint256) { return min( maxRate(), getExtraSafeEarnerRate( totalActiveOwedM, totalEarningSupply, minterRate ) ); } ``` Where: - `getExtraSafeEarnerRate` applies the rate multiplier to the safe earner rate - `RATE_MULTIPLIER` is 9,800 (98% in basis points) - `ONE` is 10,000 (100% in basis points) - `maxRate()` returns the governance-set maximum earner rate #### Safe Rate Derivation The safe earner rate is calculated using two different approaches based on the relationship between total active owed $M and total earning supply: 1. **When totalActiveOwedM ≤ totalEarningSupply**: - Uses an instantaneous cashflow approach - `safeRate = totalActiveOwedM * minterRate / totalEarningSupply` - Ensures interest paid to earners never exceeds interest collected from minters 2. **When totalActiveOwedM > totalEarningSupply**: - Uses a time-integrated approach over a 30-day confidence interval - Accounts for compound interest effects - Applies complex mathematical formula to ensure long-term solvency - Allows earner rate to temporarily exceed minter rate while maintaining system safety #### Extra Safety Factor The EarnerRateModel applies an additional safety factor to the calculated safe rate: ```solidity function getExtraSafeEarnerRate( uint240 totalActiveOwedM_, uint240 totalEarningSupply_, uint32 minterRate_ ) public pure returns (uint32) { uint256 safeEarnerRate_ = getSafeEarnerRate(totalActiveOwedM_, totalEarningSupply_, minterRate_); uint256 extraSafeEarnerRate_ = (safeEarnerRate_ * RATE_MULTIPLIER) / ONE; return (extraSafeEarnerRate_ > type(uint32).max) ? type(uint32).max : uint32(extraSafeEarnerRate_); } ``` This ensures that earners receive 98% of the theoretically safe rate, creating an additional buffer for protocol safety. ## Confidence Interval Approach The EarnerRateModel uses a 30-day confidence interval to determine safe rates: - Calculates the amount of interest minters will generate over 30 days - Derives maximum earner rate that ensures total interest paid doesn't exceed this amount - Accounts for the compounding effects of continuous interest - Assumes the `updateIndex()` function will be called at least once every 30 days ## Balance Between Minters and Earners The system maintains a mathematical relationship between interest flows: ```text totalInterestFromMinters = totalActiveOwedM * (e^(minterRate*dt) - 1) totalInterestToEarners = totalEarningSupply * (e^(earnerRate*dt) - 1) ``` The rate models ensure: `totalInterestToEarners ≤ 98% of totalInterestFromMinters` ## Edge Cases Handling The EarnerRateModel handles several edge cases with special logic: - When `totalActiveOwedM = 0` or `minterRate = 0`: The earner rate is set to 0 - When `totalEarningSupply = 0`: The earner rate is set to the maximum possible value (`type(uint32).max`) - When calculations might overflow: The implementation includes safeguards against numerical issues ## Yield Distribution The system distributes yield in the following manner: 1. **Minters pay interest** based on their outstanding debt at the minter rate 2. **Earners receive up to 98%** of the available interest (subject to constraints) 3. **Excess yield** (at least 2% plus any additional buffer) goes to the TTG Vault 4. The TTG Vault funds protocol operations and can distribute to governance token holders ## Rate Update Mechanism Interest rates don't change automatically when governance parameters are updated: 1. Rate changes in the TTG Registrar take effect only when `updateIndex()` is called 2. Both `$M` token and MinterGateway must update their respective indices 3. Updates occur automatically during key operations such as minting, burning, and transfers 4. Rate models recalculate rates during each update based on current system state ## Mathematical Precision The implementation includes several technical considerations: - Rates are stored in basis points (1/100 of a percent) for precision - Calculations use fixed-point math with 12 decimal places (1e12 scaling) - Exponential calculations use Pade approximants for gas-efficient computation - Conservative rounding strategies favor protocol safety ## Visuals ![Safe Earner Rate](https://docs.m0.org/images/technical-documentations/rates/rates.png) # Roles ## Introduction The M0 Protocol operates through a carefully designed system of actors, each with distinct roles and responsibilities crucial for the protocol's operation, security, and decentralized governance. Understanding these roles is key to understanding how `$M` is created, secured, utilized, and how the protocol evolves. The protocol's core functionality is implemented in several key smart contracts that these roles interact with: - **`MToken`**: The ERC20-compliant token contract for `$M`, which includes the unique earning capabilities. - **`MinterGateway`**: The interface for minting and burning, managing Minter collateral, and tracking debt obligations. - **`TTGRegistrar`**: The central configuration store managed by Two-Token Governance (TTG), holding approved addresses for roles and key protocol parameters. Below is a high-level overview of the primary roles within the M0 ecosystem, categorized by their main area of interaction. ## Overview of Key Protocol Roles The M0 ecosystem comprises several key actors, each playing a vital part: **1. `$M` Supply & Security:** - **Minters**: - **Description**: Minters are permissioned institutions responsible for creating (minting) new `$M`. They are the primary on/off-ramp to and from fiat. - **Function**: They provide eligible offchain collateral (such as U.S. backed reserves held in Special Purpose Vehicles) and register its value onchain via the MinterGateway. Based on this collateral and governance-set ratios, they can mint. They also pay interest on their minted supply, which forms the basis for the yield distributed to Earners and the protocol. - **Validators**: - **Description**: Validators are independent, trusted entities acting as a crucial security and verification layer for the protocol. - **Function**: Their primary role is to verify the offchain collateral reported by Minters, ensuring that all supply remains fully and transparently backed. They provide cryptographic attestations (signatures) for Minters' collateral updates to the MinterGateway. Validators also possess emergency powers, such as freezing a Minter or canceling a suspicious mint proposal, to safeguard protocol integrity. **2. `$M` Usage & Yield:** To earn yield, a user has to get the earner status. Below is an overview of the Earner status: - **Description**: Earners are addresses (users, smart contracts, or M0 Extensions) that have been approved by M0 Governance (or delegated admins for certain extensions like wM) to accrue yield on their `$M` balances. - **Function**: Once approved and activated via MToken.startEarning(), their balance (or the balance of an M0-powered extension they hold) automatically increases over time due to continuous compounding, based on the prevailing earner rate. This mechanism allows `$M` to function as a yield-bearing component for authorized participants. **3. Protocol Governance:** - **Power Token (POWER) Holders**: - **Description**: POWER Holders are the primary participants in the operational governance of the M0 Protocol. - **Function**: POWER tokens are used to vote on proposals in the `StandardGovernor` (e.g., approving `Minters`, `Validators`, `Earners`; setting operational parameters like interest rates or collateral ratios) and the `EmergencyGovernor` (for urgent protocol adjustments). Active and complete participation in `StandardGovernor` voting is incentivized through POWER token inflation and ZERO token rewards. - **Zero Token (ZERO) Holders**: - **Description**: ZERO Holders possess meta-governance authority, effectively acting as the highest level of governance oversight for the protocol. - **Function**: ZERO holders are entitled to claim a pro-rata share of protocol revenues (e.g., excess Minter yield, Power token auction proceeds) accumulated in the DistributionVault. Additionally, ZERO tokens are used to vote in the ZeroGovernor on fundamental changes, such as upgrading the core governance contracts themselves, altering critical system parameters (like governance thresholds), or changing the accepted CashTokens for proposal fees. ![Roles](https://docs.m0.org/images/technical-documentations/roles/roles.png) ## Minters ### Role Definition Minters are entities authorized to generate `$M` by securing Eligible Collateral in offchain Special Purpose Vehicles (SPVs) and maintaining an onchain Collateral Value that represents these offchain assets. ### Key Functions Minters interact with the protocol primarily through the `MinterGateway` contract: ### 1. Activation and Status Management - `activateMinter(address minter_)`: A newly approved (by governance) minter must be activated before they can mint. This explicit step stores the active status locally within `MinterGateway` for gas efficiency, avoiding repeated external checks. - Once deactivated, a minter cannot be reactivated. This permanence also optimizes gas by eliminating the need for further status checks against the external `TTGRegistrar`. ### 2. Collateral Management - `updateCollateral(collateral, retrievalIds, metadataHash, validators, timestamps, signatures)`: Updates collateral value with validator signatures - `proposeRetrieval(collateral)`: Proposes to retrieve excess collateral - Must maintain sufficient collateral to support minted `$M` tokens according to the governance-set mint ratio ### 3. Minting Process - `proposeMint(amount, destination)`: Proposes to mint a specific amount of tokens - `mintM(mintId)`: Executes a previously proposed mint after the mint delay period and before mint time-to-live period ### Lifecycle and Constraints Minters go through several states: 1. **Inactive**: Approved by governance but not activated 2. **Active**: Called `activateMinter()` and can mint tokens 3. **Frozen**: Temporarily restricted from minting (but can still burn) 4. **Deactivated**: Permanently removed from the system Minters face several key constraints: - Must update collateral within the required interval (typically daily). As of today, the update collateral interval is set to 30 hours as per [this proposal](https://governance.m0.org/proposal/17943707564765307947230727208177770356513863441428702266086965014917482113266){rel=""nofollow""}. - Must maintain collateralization above the mint ratio - Pay interest on minted tokens at the minter rate - Face penalties for missed updates or undercollateralization ### Penalties Minters can receive penalties in two cases: 1. **Missed Updates Penalty**: Applied when minters fail to update collateral within required intervals - Calculated as `penaltyRate * principalOfActiveOwedM * missedIntervals` - Charged only once per missed interval 2. **Undercollateralization Penalty**: Applied when active owed `$M`exceeds allowed maximum - Calculated as `penaltyRate * principalOfExcessOwedM * timeSpan / updateCollateralInterval` - Proportional to the duration of undercollateralization ## Validators ### Role Definition Validators serve as a critical security layer, verifying the accuracy of Minters' collateral updates and having emergency powers to protect the protocol. They are trusted entities responsible for bridging the gap between offchain assets and onchain accounting. ### Key Functions Validators interact with the protocol primarily through signature generation and special authority functions: ### 1. Collateral Verification - Sign offchain messages (`updateCollateral` digest) verifying collateral updates, which includes the list of retrieval proposals being resolved. - Signatures include timestamps to prevent replay attacks - Multiple validators must sign collateral updates to meet the threshold ### 2. Security Controls - `cancelMint(address minter, uint256 mintId)`: Cancel suspicious mint proposals - `freezeMinter(address minter)`: Temporarily freeze a minter's ability to mint new tokens ### 3. Signature Requirements - Signatures must be provided in ascending order by validator address - Each signature must have a timestamp newer than the last signature from that validator - The protocol enforces a threshold of validator signatures for valid collateral updates ### Security Mechanisms Validators implement several security protections: - **Timestamp Verification**: Ensures signatures aren't reused or used out of order - **Signature Uniqueness**: Each validator can only provide one valid signature per collateral update - **Emergency Powers**: Ability to cancel mints and freeze Minters to mitigate potential harm ## Earners ### Role Definition Earners are approved addresses that can enable earning on their `$M` token balance, allowing them to receive yield based on the protocol's earning rate. This is a key feature that separates `$M` from traditional stablecoins. ### Key Functions Earners interact with the protocol primarily through the `MToken` contract: ### 1. Earning Status Management - `startEarning()`: Converts a regular balance to an earning balance (requires TTG approval) - `stopEarning()`: Converts an earning balance back to a regular balance (keeps all accrued interest) - `stopEarning(address)`: Safety function allowing anyone to stop earning for an account that's been removed from the approved list ### 2. Balance Mechanism When an account is in earning status: - Their balance automatically increases over time through continuous compounding - Balances are stored internally as "principal amounts" that grow with the earning index - No transactions are needed to accrue interest ### Rate Mechanics Earners receive yield based on the Earner Rate, which is determined by a sophisticated rate model: - The rate model ensures yield to earners never exceeds interest paid by minters - Uses a mathematical formula with both instantaneous and time-integrated approaches - Applied with a safety factor (98% of the theoretical safe rate) - Continuously updated when key functions are called ## Regular `$M` Holders ### Role Definition Regular `$M` holders use it as a stablecoin without the earning feature. Their balances remain static and only change through explicit transactions. ### Key Functions Regular `$M` holders have standard ERC20 capabilities with additional features: ### Token Operations - Standard transfers (`transfer`, `transferFrom`), approvals (`approve`), and balance inquiries (`balanceOf`). - Support for gasless approvals through EIP-2612 permits (`permit`). - Support for transfers with authorization through EIP-3009 (`transferWithAuthorization`, `receiveWithAuthorization`). ### Transfer Mechanics Transfers between different types of accounts follow specific rules: - Between two non-earning accounts: Standard token transfer. - Between non-earning and earning accounts: Balance conversion occurs, using protocol-favoring rounding (principal rounded down on receive, principal rounded up on send). - All transfers are subject to consistency checks and appropriate balance updates in the `MToken` contract. **Note on Minter Debt Repayment**: Regular `$M` holders can choose to interact with the `MinterGateway`'s `burnM` function. This allows them to burn their *own* tokens to help repay the outstanding debt of a specific Minter (either active or deactivated), acting as an indirect way to support protocol stability or participate in the informal liquidation of a deactivated minter's position. This is distinct from standard token operations. ## Power Token Holders ### Role Definition Power token holders act as the primary governance participants, responsible for day-to-day protocol governance through the `StandardGovernor` and emergency actions through the EmergencyGovernor. ### Key Functions Power token holders can influence the protocol through several mechanisms: ### 1. Standard Governance - Vote on adding/removing addresses from minter/validator/earner lists - Vote on key protocol parameters like mint ratio, collateral update interval, etc. - Propose and vote on protocol parameter changes - `StandardGovernor` proposals require a simple majority to pass (yes votes > no votes) ### 2. Emergency Governance - Vote on emergency actions through the EmergencyGovernor - Can take immediate action to protect the protocol in emergency situations - EmergencyGovernor uses a threshold ratio mechanism requiring a minimum percentage of yes votes ### 3. Token Mechanics - Power token is inflationary (10% per active voting epoch) - Token holders can directly vote or delegate their voting power - Snapshots of balances, voting powers, and delegations occur at epoch boundaries - Inflation rewards are only given to those who vote on ALL proposals in a voting epoch - Cannot transfer/delegate/mint during odd epochs (voting epochs) - Voting power is calculated from the previous epoch's balance ### 4. Dutch Auction - Can buy Power tokens during Dutch auctions during non-voting epochs - Price decreases exponentially over time, with the slope halving every period - Purchase price is relative to the previous epoch's total supply - Payments go to the Distribution Vault ## Zero Token Holders ### Role Definition Zero token holders possess meta-governance rights, essentially serving as the highest level of governance in the protocol. They have the unique ability to completely redeploy the governance system and receive rewards from excess yield and other protocol fees. ### Key Functions Zero token holders have special privileges in the protocol: ### 1. Meta-Governance - Can vote to completely redeploy the standard and emergency governance systems through the ZeroGovernor - Can reset governance to either the current Power token or Zero token holders (complete governance reset) - Can update the threshold ratio for proposals in ZeroGovernor or EmergencyGovernor - Can set new cashTokens for the entire system - ZeroGovernor proposals require a minimum threshold of votes to pass ### 2. Reward Mechanism - Receive Zero token rewards for participation in `StandardGovernor` voting - Claim excess yield and fees from the Distribution Vault - Buy Power token with a discount during Dutch auctions ### 3. Token Mechanics - ERC20 token with 6 decimals - Supports EIP-3009, EIP-712 with EIP-1271 support, and EIP-5805 - Can only be minted by `StandardGovernor` as reward for voting participation - Used for voting in ZeroGovernor - Epochs are 15 days long with snapshots taken at the end of each epoch ## Role Interactions and System Balance The M0 Protocol maintains balance through carefully designed interactions between roles: ### Minter-Validator Relationship - Minters propose collateral updates and mint requests - Validators verify collateral and can intervene when necessary - This creates a checks-and-balances system for minting activity ### Minter-Earner Balance - Minters pay interest on borrowed tokens (minter rate) - Earners receive interest on their holdings (earner rate) - Rate models ensure earner yield never exceeds minter interest obligations ### Governance Hierarchy - Power token holders control standard parameter changes through `StandardGovernor` - Zero token holders have meta-governance capabilities through ZeroGovernor - EmergencyGovernor provides quick response capability for urgent issues - This creates a tiered governance structure for different types of decisions ### Distribution Vault Mechanism - The DistributionVault receives excess yield generated by Minters not distributed to Earners - It also receives cash tokens from Power token auctions and unrefunded proposal fees - Zero token holders can claim their pro-rata share of these accumulated assets - This provides an incentive mechanism for Zero token holders to participate in governance ### Cash Token System - Cash tokens are used to pay proposal fees in `StandardGovernor` - They're also used to purchase Power tokens in Dutch auctions - Expected to be WETH and `$M` according to the Whitepaper - Must be in an allowed set defined at ZeroGovernor deployment - Can be changed via ZeroGovernor proposals ### Economic Incentives - Minters are incentivized to maintain proper collateralization - Validators are trusted to verify offchain collateral accurately - Earners benefit from yield generated by the system - Power token holders are rewarded with inflation for active participation in all proposals - Zero token holders can claim excess yield and fees while maintaining meta-governance rights ## Trust Model The protocol operates with the following trust assumptions: - **Validators**: Fully trusted to verify offchain collateral accurately and monitor onchain activities. They must be reachable and able to issue valid signatures for minters in a timely manner. - **Minters**: Trusted to maintain their offchain collateral and meet protocol obligations. Minters are expected to update their collateral at required intervals and pay any imposed penalties. - **`$M` holders**: Not trusted, but can freely use the token as a stablecoin without special permissions. - **Power token holders**: Trusted to act in the best interest of the protocol for day-to-day governance. They are expected to vote on proposals and participate actively in governance. - **Zero token holders**: Trusted with meta-governance authority to reset the entire governance system if necessary. They have the highest level of governance power and are assumed to act as responsible stewards of the protocol. - **CashTokens**: Expected to be WETH and `$M` as stated in the Whitepaper, but nothing prevents deployment with other cashTokens. These must be ERC20-compliant without special behavior (such as transfer hooks, rebasing mechanisms, or transfer fees). The protocol incorporates several safeguards against potential trust violations: 1. Multi-level governance (`StandardGovernor`, EmergencyGovernor, ZeroGovernor) 2. Collateral verification through multiple validators 3. Penalty systems for minters who don't follow protocol rules 4. Economic incentives aligning with protocol health for all participants This trust model creates a balanced system where each role has appropriate permissions and incentives to maintain protocol stability and security. # M0 On Solana This document provides a detailed technical overview of the M0 Protocol's implementation on the Solana blockchain. It is intended for developers who want to integrate with or build on top of `$M` and its extensions on Solana. It assumes you are familiar with Solana's programming model (programs, accounts, CPIs) and have a conceptual understanding of M0's hub-and-spoke architecture. ## Architecture & Core Components M0's Solana deployment functions as a "spoke" in the cross-chain model, with Ethereum as the "hub". Communication and token transfers are managed by the **M Portal**, which is a customized implementation of Wormhole's **Native Token Transfer (NTT)** framework. The system is composed of three core onchain programs and several offchain services that work in concert. ![M0 Solana Program Architecture](https://docs.m0.org/images/technical-documentations/solana/solana_m_programs.png) ### Onchain Programs The onchain logic is modular, distributed across the following main Solana programs written in Rust using the Anchor framework. | Program | Mainnet Program ID | Devnet Program ID | Description | | ----------------- | ---------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Portal** | `mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY` | `mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY` | The gateway for bridging `$M` between EVM chains and Solana. A fork of Wormhole's NTT program with M0-specific payload support and executor integration. | | **Earn** | `mz2vDzjbQDUDXBH6FPF5s4odCJ4y8YLE5QWaZ8XdZ9Z` | `mz2vDzjbQDUDXBH6FPF5s4odCJ4y8YLE5QWaZ8XdZ9Z` | Manages yield distribution and earner permissions for the base `$M` token with frozen-by-default security model. | | **M Extensions** | `3C865D264L4NkAm78zfnDzQJJvXuU3fMjRUvRxyPi5da` | `3C865D264L4NkAm78zfnDzQJJvXuU3fMjRUvRxyPi5da` | Unified extension framework (`m_ext`) compiled with feature flags for NoYield, ScaledUi, and Crank models. | | **Swap Facility** | `MSwapi3WhNKMUGm9YrxGhypgUEt7wYQH3ZgG32XoWzH` | `MSwapi3WhNKMUGm9YrxGhypgUEt7wYQH3ZgG32XoWzH` | Permissioned router for atomic swaps between whitelisted M0 extensions (`ext_swap` program). | ### Key Onchain Assets | Asset | Mainnet Mint Address | Devnet Mint Address | Description | | --------------- | --------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | **`$M` Token** | `mzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJo` | `mzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6` | The base yield-bearing stablecoin, implemented as a Token-2022 asset with frozen-by-default security. | | **`$wM` Token** | `mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp` | `mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp` | The wrapped, 1:1 `$M`-backed extension using the Crank model, also a Token-2022 asset. | ## V2 Upgrades: What Changed The V2 upgrade introduced seven major enhancements to the Solana implementation: ### 1. Direct Bridging to Extensions **What Changed:** The Portal now supports a `destination_token` field in the `AdditionalPayload`, allowing users to bridge directly to extension tokens (like `$wM`) instead of only receiving base `$M`. **Why It Matters:** Eliminates the need for a separate wrap transaction after bridging, improving UX and reducing costs. **Technical Implementation:** ```rust pub struct AdditionalPayload { pub index: u64, pub destination_token: [u8; 32], // NEW: Target mint address (M or extension) pub earner_root: Option<[u8; 32]>, } ``` ### 2. Frozen-by-Default `$M` **What Changed:** All new `$M` token accounts are initialized with `AccountState::Frozen` using Token-2022's `DefaultAccountState` extension. **Why It Matters:** Enhances security by preventing unauthorized accounts from holding `$M`. Only approved earners (verified via Merkle proof) can have their accounts thawed. **Technical Implementation:** ```rust // $M mint is created with DefaultAccountState::Frozen let default_state = DefaultAccountState { state: AccountState::Frozen, }; ``` ### 3. Onchain Yield Distribution **What Changed:** Yield distribution is now fully onchain via the scaled-ui mechanism, replacing V1's centralized distribution. **Why It Matters:** Increases transparency and decentralization. All yield calculations and distributions are verifiable onchain. **Technical Implementation:** - Yield distributed via scaled-ui mechanism - Index updates propagated onchain - Extensions automatically sync on wrap/unwrap operations ### 4. Permissionless Index Updates **What Changed:** The `sync()` instruction for extension index updates is now permissionless -- anyone can call it. Additionally, anyone can bridge an index update, and extensions automatically sync on wrap/unwrap operations. **Why It Matters:** Enables keeper bots and users to ensure yield indices are always up-to-date without relying on centralized services. **Who Can Call:** Anyone (no signer required for sync, though accounts must be valid). Index updates can also be bridged permissionlessly. ### 5. Executor Support for Generic Bridge Messages **What Changed:** Integration with Wormhole's executor pattern (`executor-account-resolver-svm`) enables arbitrary cross-chain instructions to be executed atomically with bridge transfers. **Why It Matters:** Unlocks advanced cross-chain composability, such as "bridge and stake" or "bridge and swap" in a single transaction. **Technical Implementation:** - Portal validates executor messages - Executor Account Resolver resolves required accounts - Arbitrary CPI instructions executed after mint ### 6. Unified Extension Framework **What Changed:** All extensions (NoYield, ScaledUi, Crank) are now compiled from a single `m_ext` program using Rust feature flags. **Why It Matters:** Reduces code duplication, simplifies auditing, and ensures consistent behavior across all extension types. **Feature Flags:** ```rust [features] no-yield = [] scaled-ui = [] crank = [] ``` ### 7. Multi-Network Support (Fogo Compatibility) **What Changed:** V2 architecture is designed to support both Solana and Fogo (Wormhole's native rollup) with minimal changes. **Why It Matters:** Future-proofs the protocol for multi-network deployment and leverages Wormhole's native cross-chain capabilities. **Implementation:** Feature flags enable Fogo-specific configurations without forking the codebase. ## Program Specifications ### 1. Portal Program (`programs/portal`) The Portal is the entry and exit point for `$M` into the Solana ecosystem. #### Core Logic & Custom Payload - **Core Logic:** Based on Wormhole NTT, it validates Wormhole Verifiable Action Approvals (VAAs) and processes inbound/outbound transfers. The key inbound instruction is `release_inbound_mint_multisig`, which mints tokens and makes a CPI to the `Earn` program's `propagate_index` function. - **Custom Payload (`payloads/token_transfer.rs`):** The standard `NativeTokenTransfer` struct is extended with an `AdditionalPayload`. This is the most critical customization, allowing M0-specific data to be securely transmitted with every bridge transfer. ```rust // programs/portal/src/payloads/token_transfer.rs pub struct AdditionalPayload { pub index: u64, // Latest $M Earning Index from Ethereum pub destination_token: [u8; 32], // Target token mint (M or extension) pub earner_root: Option<[u8; 32]>, // Merkle root of approved earners } ``` - **`index`:** The latest `$M` Earning Index from Ethereum. - **`destination_token`:** The target token mint address on Solana (e.g., `$M` or `$wM`). **NEW in V2.** - **`earner_root`:** An optional Merkle root of the official `$M` earner list, as determined by M0 Governance on Ethereum. #### Executor Integration V2 Portal integrates with Wormhole's executor pattern to enable generic cross-chain instructions: ```rust // Executor message structure pub struct ExecutorMessage { pub instruction_data: Vec, pub program_id: Pubkey, pub account_metas: Vec, } ``` The executor account resolver (`executor-account-resolver-svm`) resolves required accounts and executes instructions atomically with bridge transfers. ### 2. Earn Program (`programs/earn`) This program manages yield and permissions for the base `$M` token on Solana. #### V2 Security Model: Frozen by Default All token accounts are initialized with `AccountState::Frozen`. Accounts are only thawed after: 1. User submits Merkle proof via `add_registrar_earner` 2. Proof is verified against `earner_merkle_root` 3. Account state is updated to `AccountState::Initialized` #### State (`state/`) - **`Global` Account (`state/global.rs`):** A PDA seeded with `b"global"`that stores the program's configuration. - `admin`: The administrative authority. - `earn_authority`: A permissioned key that can call `claim_for` instructions. - `index`: The most recently propagated `$M` Earning Index. - `claim_cooldown`: The minimum time between yield claim cycles. - `max_yield` & `distributed`: Used to track and cap the amount of yield distributed in a cycle. - `claim_complete`: A flag indicating if the current claim cycle has finished. - `earner_merkle_root`: The Merkle root of the governance-approved earner list. - **`Earner` Account (`state/earner.rs`):** A PDA seeded with `b"earner"`and the user's token account address. It tracks an individual's earning status. - `last_claim_index`: The index at which the user last received yield. - `user`: The wallet address of the earner. - `user_token_account`: The specific token account that is earning yield. #### Key Instructions (`instructions/`) - **`propagate_index`:** Called by the Portal via CPI. It updates the `Global` account's `index` and `earner_merkle_root`, and can initiate a new claim cycle if conditions are met (cooldown passed, new yield available). - **`add_registrar_earner`:** A **permissionless** instruction allowing a user to prove their eligibility to earn yield. The user provides a Merkle proof that their address is included in the `earner_merkle_root`. **On success, thaws the frozen token account.** - **`claim_for`:** A permissioned instruction called by the `earn_authority`. It calculates the yield owed to a specific earner since their last claim and mints new tokens to their account. **NEW in V2: fully onchain distribution.** - **`complete_claims`:** Called by the `earn_authority` to mark the end of a yield distribution cycle. - **`sync`:** **NEW in V2.** Permissionless instruction to update the earning index. Anyone can call this to keep indices current. ### 3. `$M` Extension Framework (`programs/m_ext`) **NEW in V2:** Unified extension framework replacing separate programs for each model. #### Architecture All extension models are compiled from a single `m_ext` program using Rust feature flags: ```rust // Cargo.toml [features] no-yield = [] scaled-ui = [] crank = [] ``` **Deployed Instances:** - **NoYield Extensions:** Compiled with `--features no-yield` - **ScaledUi Extensions:** Compiled with `--features scaled-ui` - **Crank Extensions:** Compiled with `--features crank` (used by `$wM`) #### State Structures **ExtGlobal Account (shared across all models):** ```rust pub struct ExtGlobal { pub admin: Pubkey, pub earn_authority: Option, // Only for Crank model pub ext_mint: Pubkey, pub m_mint: Pubkey, pub m_earn_global_account: Pubkey, pub bump: u8, pub m_vault_bump: u8, pub ext_mint_authority_bump: u8, pub yield_config: YieldConfig, pub wrap_authorities: Vec, } pub struct YieldConfig { pub variant: YieldVariant, // NoYield | ScaledUi | Crank pub last_m_index: u64, pub last_ext_index: u64, } pub enum YieldVariant { NoYield, ScaledUi, Crank, } ``` **Crank-Specific State:** ```rust // Only present when compiled with 'crank' feature pub struct EarnManager { pub manager: Pubkey, pub fee_bps: u64, pub fee_token_account: Pubkey, pub bump: u8, } pub struct Earner { pub user: Pubkey, pub user_token_account: Pubkey, pub recipient_token_account: Pubkey, pub earn_manager: Pubkey, pub last_balance: u64, pub last_index: u64, pub bump: u8, } ``` #### Core Instructions (Shared) - **`initialize`:** Sets up the extension's global state - **`wrap(amount: u64)`:** Wraps into extension token - **`unwrap(amount: u64)`:** Unwraps extension token back - **`add_wrap_authority` / `remove_wrap_authority`:** Manage wrap permissions - **`transfer_admin`:** Transfer admin control #### Model-Specific Instructions **NoYield:** - `claim_fees()`: Admin claims all accrued yield **ScaledUi:** - `sync()`: Permissionless index update (updates scaled-ui rate) - `set_fee(fee_bps: u64)`: Set optional fee (typically 0 for full pass-through) **Crank:** - `add_earn_manager` / `remove_earn_manager`: Admin manages earn managers - `add_earner` / `remove_earner`: Earn manager manages earners - `claim_for(user, snapshot_balance)`: Earn authority distributes yield - `sync()`: Update extension index from base `$M` program - `set_recipient`: Earner sets custom yield recipient ### 4. Swap Facility (`programs/ext_swap`) **NEW in V2:** Centralized router for atomic swaps between whitelisted extensions. #### Global State ```rust pub struct SwapGlobal { pub bump: u8, pub admin: Pubkey, pub whitelisted_unwrappers: Vec, pub whitelisted_extensions: Vec, } pub struct WhitelistedExtension { pub program_id: Pubkey, pub ext_global: Pubkey, } ``` #### Core Instructions - **`swap(amount, remaining_accounts_split_idx)`**: Atomic cross-extension swap - **`wrap(amount)`**: Convert `$M` to extension - **`unwrap(amount)`**: Convert extension to `$M` (requires `whitelisted_unwrapper`) - **`whitelist_extension` / `remove_whitelisted_extension`**: Admin manages extensions - **`whitelist_unwrapper` / `remove_whitelisted_unwrapper`**: Admin manages unwrap permissions **Security Model:** - Only whitelisted extensions can participate in swaps - Only whitelisted unwrappers can call `unwrap` directly - All swaps maintain 1:1 peg ## Developer Integration ### Interacting via SDK The recommended way to interact with the M0 Solana programs is through the official TypeScript SDK. - **Installation:** `pnpm i @m0-foundation/solana-m-sdk` - **Core Classes:**The SDK provides classes that abstract away the onchain complexity: - `EarnAuthority`: For admin-level actions like initiating claims and syncing indexes. - `EarnManager`: For managing a set of earners within an extension (Crank model). - `Earner`: For user-level actions and querying an individual's yield status. **Example: Building a Claim Transaction (V2 Crank Model)** ```tsx import { EarnAuthority, Earner } from "@m0-foundation/solana-m-sdk"; import { Connection, PublicKey } from "@solana/web3.js"; import { createPublicClient, http } from "viem"; // Setup clients const connection = new Connection(RPC_URL); const evmClient = createPublicClient({ transport: http(EVM_RPC_URL) }); // Load authority and earner const auth = await EarnAuthority.load(connection, evmClient, M_EXT_PROGRAM_ID); const earners = await auth.getAllEarners(); const earnerToClaim = earners[0]; // Build the claim instruction const claimIx = await auth.buildClaimInstruction(earnerToClaim); if (claimIx) { // build, sign, and send transaction with the instruction... } ``` **Example: Permissionless Sync (V2 ScaledUi Model)** ```tsx import { Program } from "@coral-xyz/anchor"; // Anyone can call sync to update yield indices const tx = await program.methods .sync() .accounts({ extGlobal: extGlobalPDA, extMint: extMintAddress, mEarnGlobal: mEarnGlobalAddress, mVault: mVaultPDA, token2022Program: TOKEN_2022_PROGRAM_ID, }) .rpc(); console.log("Synced yield:", tx); ``` ### Offchain Data via API For historical data, analytics, and building dashboards, use the M0 Solana API, which is powered by a Substreams indexer. - **Base URL (Mainnet):** `https://api-production-0046.up.railway.app` - **SDK:** `pnpm i @m0-foundation/solana-m-api-sdk` - **Key Endpoints:** - `/events/bridges`: Get latest bridge events. - `/events/index-updates`: Get historical `$M` Index updates. - `/token-account/{pubkey}/{mint}/claims`: Get yield claim history for a specific token account. ### Onchain Addresses A full list of program IDs, mints, and other key accounts for both Mainnet and Devnet can be found in the [**M0 Platform Addresses page**](https://docs.m0.org/resources/addresses/m0-platform#solana). ## Fogo Network Support **NEW in V2:** The architecture is designed to support Fogo, Wormhole's native rollup, alongside Solana. **What is Fogo?** - Wormhole-native rollup optimized for cross-chain applications - SVM-compatible (runs Solana programs with minimal changes) - Optimized for low-latency cross-chain messaging **M0 on Fogo:** - Same program logic as Solana deployment - Feature flags enable Fogo-specific optimizations - Seamless interoperability with Solana via Wormhole NTT **Deployment Status:** Fogo support is live. Check the [Addresses page](https://docs.m0.org/resources/addresses) for Fogo-specific program IDs. ## Source Code & Audits - **Portal & Earn Programs:** [m0-foundation/solana-m](https://github.com/m0-foundation/solana-m){rel=""nofollow""} - **Extension Framework & Swap Facility:** [m0-foundation/solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} - **Audits:** Security audits for all programs can be found in the [**Audits resource page**](https://docs.m0.org/resources/audits). ## Next Steps - **Build with `$M`:** Integrate the base `$M` token into your Solana dApp - **Create an Extension:** Deploy your own `$M`-backed stablecoin using NoYield, ScaledUi, or Crank models # MGUSD on Stellar MGUSD is an M0-powered Stellar Asset Contract (SAC) stablecoin. It is minted and burned via a **Soroban smart contract** (the "Minter Gateway"). The SAC holds balances and handles standard SEP-41 transfers; the minter gateway is the sole authority for supply changes, yield accrual, and compliance controls. This document is for developers and integrators who want to understand how MGUSD is minted, distributed, redeemed, and how yield accrues. It assumes familiarity with Stellar's account model, trustlines, and the Stellar Asset Contract. --- ## Architecture & Core Components MGUSD is a two-layer system: | Component | Description | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Minter Gateway (Soroban)** | The issuance control panel. Holds all roles, the yield index, and the supply accumulators. Every mint, burn, yield claim, rate change, and compliance action goes through it. | | **Stellar Asset Contract (SAC)** | The stablecoin token. Holds balances, enforces authorization (`AUTH_REQUIRED`), and exposes the standard SEP-41 `transfer()`. The Minter Gateway is the SAC admin and drives it via cross-contract calls. | The Minter Gateway never exposes a `transfer()` of its own — holders move tokens using the SAC's standard SEP-41 interface directly. The gateway's job is to be the single, authoritative gateway for everything that changes the supply or compliance state. ### Actors | Actor | Responsibility | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Bridge** | The primary operator — holds **Admin**, **Minter**, **Yield Recipient Manager**, and **Forced Transfer Manager**, and acts as a **block / unblock operator** and a **pauser**. | | **MoneyGram** | The **Yield Recipient** (receives claimed yield) and the off-chain on/off-ramp for end users. | | **M0** | Co-holds the **Pauser** role. | | **Predicate** | Compliance partner — a **block / unblock operator** for onchain screening. | --- ## Lifecycle Flows ### 1. Minting (Bridge → Treasury) 1. MoneyGram receives fiat from an end user and notifies the Bridge. 2. Bridge calls `mint(caller, treasury_address, amount)` on the Minter Gateway. 3. The gateway finalizes pending yield, increases `total_principal` and `total_supply`, then mints SAC tokens to the treasury address. 4. The treasury now holds MGUSD stablecoin. ### 2. User Distribution (Treasury → End User) 1. An unblock operator whitelists accounts — individually via `unblock_user`, or in batch via `batch_unblock_users` (up to 40 per call). 2. The treasury transfers tokens to the user via the SAC's standard SEP-41 `transfer()`. 3. Whitelisted (unblocked) accounts can freely transfer among themselves. 4. Non-whitelisted (blocked) accounts cannot send or receive tokens. ### 3. Redemption (End User → MoneyGram → Bridge) 1. The end user initiates redemption through MoneyGram. 2. Bridge calls `burn(caller, user_address, amount)` — removing tokens at the SAC layer via clawback. 3. The Minter Gateway finalizes pending yield and decreases both accumulators. 4. MoneyGram sends fiat to the end user off-chain. ### 4. Yield Claiming 1. Bridge calls `set_interest_rate(caller, rate_bps)` to set the current interest rate (this is a **Minter** permission, not Admin). 2. Yield accrues continuously on `total_principal` using the exponential index. 3. The Yield Recipient Manager (Bridge) calls `claim_yield(caller)` to mint accrued yield as new SAC tokens to the Yield Recipient (MoneyGram). 4. Claimed yield increases `total_supply` but **not** `total_principal` — it does not compound. ### 5. Forced Transfer (Compliance Action) 1. The Forced Transfer Manager identifies a need to move tokens between accounts. 2. It invokes `force_transfer(caller, from, to, amount)` — no authorization from the source account is required. 3. The Minter Gateway claws back tokens from the source and mints them to the destination at the SAC layer. 4. Accumulators are unchanged — this is a balance redistribution, not a supply change. 5. Works even if the source account is blocked. --- ## Roles | Role | Permissions | Held by | | --------------------------------- | ------------------------------------------------- | ----------------- | | **Admin** | Role administration only (see below) | Bridge | | **Minter** | `mint`, `burn`, `set_interest_rate` | Bridge | | **Yield Recipient Manager** | `set_yield_recipient`, `claim_yield` | Bridge | | **Yield Recipient** | Passive — receives tokens minted by `claim_yield` | MoneyGram | | **Forced Transfer Manager** | `force_transfer` | Bridge | | **Block operator** (membership) | `block_user`, `batch_block_users` | Bridge, Predicate | | **Unblock operator** (membership) | `unblock_user`, `batch_unblock_users` | Bridge, Predicate | | **Pauser** (membership) | `pause`, `unpause` | M0, Bridge | ::note **Admin is not a super-role.** Admin's powers are limited to role administration: `set_admin`, `set_minter`, `set_yield_recipient_manager`, `set_forced_transfer_manager`, the block / unblock / pauser membership management functions, `reconcile_burn`, `transfer_sac_admin`, and `upgrade`. Admin **cannot** mint, burn, set the rate, block users, force-transfer, claim yield, or pause without first granting itself the relevant role. There is no implicit emergency fallback — operational procedures must ensure the dedicated role-signers remain reachable. :: Every role-gated function calls `require_auth()` on its `caller` argument and verifies the caller equals the designated role holder — no implicit trust and no admin override. All roles are single-address except **Block operator**, **Unblock operator**, and **Pauser**, each of which is a membership set that Admin grants and revokes. --- ## Contract Interface > Each role can only call its own functions. Admin is not a super-role and cannot call non-admin functions without first granting itself the relevant role. ### Admin-exclusive functions | Function | Signature | Description | | -------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------- | | `set_admin` | `(new_admin: Address)` | Transfer the admin role to a new address | | `set_minter` | `(new_minter: Address)` | Set a new minter address | | `set_yield_recipient_manager` | `(new_yrm: Address)` | Set a new yield recipient manager | | `set_forced_transfer_manager` | `(new_ftm: Address)` | Set a new forced transfer manager | | `add_block_operator` / `remove_block_operator` | `(addr: Address)` | Grant / revoke **block** permission (membership set; idempotent) | | `add_unblock_operator` / `remove_unblock_operator` | `(addr: Address)` | Grant / revoke **unblock** permission (membership set; idempotent) | | `add_pauser` / `remove_pauser` | `(addr: Address)` | Grant / revoke **pause** permission (membership set; idempotent) | | `reconcile_burn` | `(amount: i128)` | Decrease both accumulators to reconcile tokens destroyed outside the contract | | `transfer_sac_admin` | `(new_sac_admin: Address)` | Transfer SAC admin role from the Minter Gateway to another address | | `upgrade` | `(new_wasm_hash: BytesN<32>)` | Upgrade the contract WASM to a new version | ### Minter functions | Function | Signature | Description | | ------------------- | ------------------------------------------------ | ----------------------------------------------------------- | | `mint` | `(caller: Address, to: Address, amount: i128)` | Mint SAC tokens and increase both accumulators | | `burn` | `(caller: Address, from: Address, amount: i128)` | Remove SAC tokens (clawback) and decrease both accumulators | | `set_interest_rate` | `(caller: Address, rate_bps: u32)` | Set the interest rate in basis points (max 5000 = 50%) | ### Forced Transfer Manager functions | Function | Signature | Description | | ---------------- | ------------------------------------------------------------- | -------------------------------------------------------- | | `force_transfer` | `(caller: Address, from: Address, to: Address, amount: i128)` | Force-move SAC tokens between accounts (clawback + mint) | ### Yield Recipient Manager functions | Function | Signature | Description | | --------------------- | ------------------------------------ | ---------------------------------------------------------------- | | `set_yield_recipient` | `(caller: Address, new_yr: Address)` | Set the address that receives claimed yield | | `claim_yield` | `(caller: Address) -> i128` | Claim accrued yield; mints new SAC tokens to the yield recipient | ### Block / unblock (allowlist) functions `block_user` / `batch_block_users` require a **block operator**; `unblock_user` / `batch_unblock_users` require an **unblock operator**. Backed by the SAC allowlist. | Function | Signature | Description | | --------------------- | ------------------------------------------ | -------------------------------------------------- | | `block_user` | `(user: Address, operator: Address)` | Block a user on the SAC (`set_authorized(false)`) | | `unblock_user` | `(user: Address, operator: Address)` | Unblock a user on the SAC (`set_authorized(true)`) | | `batch_block_users` | `(users: Vec
, operator: Address)` | Block up to 40 users in a single transaction | | `batch_unblock_users` | `(users: Vec
, operator: Address)` | Unblock up to 40 users in a single transaction | ### Pauser functions | Function | Signature | Description | | --------- | ------------------- | ------------------------------------------------------------------------------ | | `pause` | `(caller: Address)` | Pause the contract — blocks `mint`, `burn`, `claim_yield`, `set_interest_rate` | | `unpause` | `(caller: Address)` | Unpause the contract — resumes all blocked operations | ### View functions `admin`, `minter`, `yield_recipient_manager`, `yield_recipient`, `forced_transfer_manager`, `is_block_operator`, `is_unblock_operator`, `is_pauser`, `sac_token`, `interest_rate`, `current_index`, `latest_index`, `accrued_yield`, `total_principal`, `total_supply`, `blocked(account)`, `balance(id)`, and `paused`. ### Initialization The contract is initialized via `__constructor` during deployment. It takes the SAC address plus eight role addresses (admin, minter, yield recipient manager, yield recipient, forced transfer manager, and one seed address each for the block operator, unblock operator, and pauser membership sets). The same address may be reused across arguments when one key should hold multiple permissions. The yield index starts at `1.0` (`INDEX_SCALE`) on first use. --- ## Yield Mechanics ### Continuous index model Yield accrues continuously using an exponential index: ```text currentIndex = latestIndex × e^(rate × elapsed / SECONDS_PER_YEAR) ``` Where `latestIndex` is the last stored index (initialized to `1.0`, scaled as `1e12`), `rate` is the annual rate converted from basis points, `elapsed` is seconds since the last update, and `SECONDS_PER_YEAR` is `31,536,000`. The `e^x` term uses a 4th-order Taylor series, accurate for all realistic rates. ### Two accumulators | Accumulator | Tracks | Modified by | | ----------------- | -------------------------------------------------- | ----------------------------------------------- | | `total_principal` | Yield-earning base (mints − burns) | `mint`, `burn`, `reconcile_burn` | | `total_supply` | All outstanding tokens (principal + claimed yield) | `mint`, `burn`, `reconcile_burn`, `claim_yield` | Only `total_principal` earns yield. The accrued amount is `total_principal × (newIndex − oldIndex) / INDEX_SCALE`, accumulated in `accrued_yield` on every index update. ### Non-compounding When `claim_yield()` is called, pending yield is finalized, `accrued_yield` is captured and reset to zero, `total_supply` increases by the claimed amount, and new SAC tokens are minted to the yield recipient. `total_principal` is **unchanged** — claimed yield does not earn more yield. ::note The index is updated **before** every state-changing operation (`mint`, `burn`, `reconcile_burn`, `claim_yield`, `set_interest_rate`). This guarantees yield is finalized at the correct principal and rate before any change takes effect. :: --- ## Transfers & the Issuer Burn Problem Holders move MGUSD using the SAC's standard SEP-41 `transfer()` — both sender and receiver must be authorized (unblocked) for a transfer to succeed. Transfers happen entirely at the SAC layer and do **not** change `total_principal` or `total_supply`; they are balance redistributions, not mints or burns. On classic Stellar, sending tokens to the **issuer address** burns them automatically. If MGUSD is sent to the issuer, tokens are destroyed at the SAC layer but the Minter Gateway's accumulators are never updated — so yield would keep accruing on phantom principal. ### Mitigation: AUTH\_REQUIRED + whitelist The SAC is configured with `AUTH_REQUIRED`, so all accounts start unauthorized (blocked) by default. Accounts can only transact after an unblock operator calls `unblock_user()`, which limits who can move tokens at all. ::warning The issuer account is **exempt from `AUTH_REQUIRED`** at the Stellar protocol level. Authorized users **can** still send tokens directly to the issuer, destroying them without updating the accumulators. The whitelist reduces accidental issuer burns but does not eliminate them. If it happens, Admin calls `reconcile_burn(amount)` to decrease both accumulators and bring the Minter Gateway's bookkeeping back in line with the actual circulating supply. :: --- ## Compliance & Safety ### Block / unblock The SAC operates in `AUTH_REQUIRED` mode — accounts are blocked by default. `unblock_user` maps to SAC `set_authorized(true)` and `block_user` to `set_authorized(false)`. Only a block operator can block, and only an unblock operator can unblock. Batch operations (`batch_block_users` / `batch_unblock_users`) accept up to 40 users per call, are atomic (any failure reverts the whole transaction), and emit one event per user for indexer compatibility. ### Pausable The Minter Gateway implements a pause mechanism. When paused, the following revert immediately: | Blocked function | Role | | ------------------- | ----------------------- | | `mint` | Minter | | `burn` | Minter | | `set_interest_rate` | Minter | | `claim_yield` | Yield Recipient Manager | Compliance operations (`block_user`, `unblock_user`, their batch variants, and `force_transfer`) and all view functions remain fully accessible while paused, so regulatory actions can still be executed. `reconcile_burn` is also intentionally callable while paused — issuer-burn destruction happens outside Minter Gateway control and continues during a pause, so reconciliation must remain available to prevent unbounded accumulator divergence. --- ## Events Event names are the snake\_case form of the underlying contract event struct. Topic fields are marked **(topic)**; the rest are payload data. | Event | Emitted by | Fields | | ----------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------- | | `admin_set` | `set_admin` | `old` **(topic)**, `new` | | `minter_set` | `set_minter` | `old` **(topic)**, `new` | | `yield_recipient_manager_set` | `set_yield_recipient_manager` | `old` **(topic)**, `new` | | `yield_recipient_set` | `set_yield_recipient` | `old` **(topic)**, `new` | | `forced_transfer_manager_set` | `set_forced_transfer_manager` | `old` **(topic)**, `new` | | `block_operator_added` / `block_operator_removed` | `add_block_operator` / `remove_block_operator` | `addr` **(topic)** | | `unblock_operator_added` / `unblock_operator_removed` | `add_unblock_operator` / `remove_unblock_operator` | `addr` **(topic)** | | `pauser_added` / `pauser_removed` | `add_pauser` / `remove_pauser` | `addr` **(topic)** | | `interest_rate_set` | `set_interest_rate` | `rate_bps` | | `mint` | `mint` | `to` **(topic)**, `amount`, `new_total_principal`, `new_total_supply` | | `burn` | `burn` | `from` **(topic)**, `amount`, `new_total_principal`, `new_total_supply` | | `reconcile` | `reconcile_burn` | `amount`, `new_total_principal`, `new_total_supply` | | `yield_claimed` | `claim_yield` | `recipient` **(topic)**, `amount` | | `update_index` | every supply/rate mutation | `latest_index` | | `force_transfer` | `force_transfer` | `from` **(topic)**, `to` **(topic)**, `amount` | | `upgraded` | `upgrade` | `by` **(topic)**, `new_wasm_hash` | | `sac_admin_transferred` | `transfer_sac_admin` | `new_sac_admin` **(topic)** | | `user_blocked` / `user_unblocked` | block / unblock functions | `user` **(topic)** | | `paused` / `unpaused` | `pause` / `unpause` | *(no fields)* | --- ## Onchain Addresses The MGUSD Minter Gateway and SAC addresses for Stellar Mainnet and Testnet can be found on the [**MGUSD Deployments page**](https://docs.m0.org/resources/addresses/mgusd-platform). --- ## Source Code & Audits - **Minter Gateway contract & deployment tooling:** [m0-platform/mgusd-minter-gateway](https://github.com/m0-platform/mgusd-minter-gateway){rel=""nofollow""} - **Audits:** Security audit reports can be found on the [**Audits resource page**](https://docs.m0.org/resources/audits). --- ## Next Steps - **Integrate MGUSD:** Use the SAC's standard SEP-41 interface to hold and transfer MGUSD once your account is whitelisted. - **Look up addresses:** Find the Minter Gateway and SAC contract addresses on the [MGUSD Deployments page](https://docs.m0.org/resources/addresses/mgusd-platform). # Wrapped M (wM) ## Overview ### What is Wrapped `$M` (wM)? Wrapped `$M` (wM) is a non-rebasing ERC-20 wrapper for the rebasing and immutable `$M` token. It maintains yield-earning capabilities while providing compatibility with DeFi protocols that require standard ERC-20 tokens. While the native `$M` token automatically increases balances as yield accrues, Wrapped `$M` maintains static balances where yield can be explicitly claimed. This makes Wrapped `$M` compatible with protocols that aren't designed to handle rebasing tokens, dramatically expanding the utility of `$M` in the broader DeFi ecosystem. ![Wrapped M](https://docs.m0.org/images/technical-documentations/m/wM.png){.doc-logo} ### Why Wrapped M? Every M0 extension eventually wants to connect to certain utilities such as DeFi protocols, on/off ramps, etc. In order to concentrate liquidity, that utility should always exist in the form of M0's stablecoin building block. However, rebasing tokens automatically change their balances as yield accrues, which creates challenges for many DeFi protocols that expect static token balances. Wrapped `$M` solves this by: 1. Maintaining fixed token balances that don't automatically increase 2. Separately tracking and allowing manual claiming of accrued yield 3. Preserving the full economic benefits of the M0 platform while improving protocol compatibility 4. Enabling protocol-specific administration of earning capabilities ### Key Features - **DeFi Compatibility**: Works with lending platforms, AMMs, and other protocols that require standard ERC-20 behavior - **Yield Preservation**: Maintains yield-earning capabilities of the underlying `$M` token - **Value & Solvency**: wM aims for a direct value link to `$M` for earners. The target value for an earning token is its underlying `$M` principal plus accrued yield: [[[]{.katex-mathml}[[[]{.strut style="height:0.8778em;vertical-align:-0.1944em;"}[1]{.mord}[[ ]{.mord}]{.mord.text}[[earning ]{.mord}]{.mord.text}[[wM]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[wM yield ]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[≈]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.7667em;vertical-align:-0.0833em;"}[1]{.mord}[[ ]{.mord}]{.mord.text}[[M]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[Accrued Yield]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}:br Critically, the entire system maintains a solvency invariant, ensuring it's always fully backed. The total `$M` held by the wM contract is always greater than or equal to the total wM supply plus all accrued yield owed to earners: [[[]{.katex-mathml}[[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[≥]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.3591em;vertical-align:-1.6647em;"}[[[[[[]{.pstrut style="height:3em;"}[[[[Total wM Liabilities, aka wM supply + accrued yield]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-1.4715em;"}[[]{.pstrut style="height:3em;"}[[[[[[]{.pstrut style="height:3em;"}[[]{.brace-left style="height:0.548em;"}[]{.brace-center style="height:0.548em;"}[]{.brace-right style="height:0.548em;"}]{.stretchy style="height:0.548em;min-width:1.6em;"}]{.svg-align style="top:-2.1576em;"}[[]{.pstrut style="height:3em;"}[[[totalNonEarningSupply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[projectedEarningSupply]{.mord}]{.mord.text}]{.mord}]{style="top:-3em;"}]{.vlist style="height:0.6944em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.8424em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{style="top:-3em;"}]{.vlist style="height:0.6944em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.6647em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}:br This guarantee is maintained through the system's design, where yield earned on `$M` held for non-earning wM holders, along with minor positive rounding effects during transfers, accumulates as "excess" reserves within the contract. - **Flexible Yield Options**: Configurable claiming and recipient mechanisms - **Delegated Administration**: Trusted admins can manage earner status without governance - **Protocol Value Capture**: System design naturally accumulates excess reserves that benefit the broader protocol ecosystem ### Comparison to `$M` | **Feature** | **`$M`** | **Wrapped M** | | ------------------ | ---------------------------------- | --------------------------------------- | | Balance Behavior | Automatically increases (rebasing) | Remains static until explicit claim | | Earning Mechanism | Implicit (balances grow) | Explicit (yield accumulates separately) | | Yield Realization | Automatic with balance updates | Manual claiming required | | DeFi Compatibility | Limited due to rebasing | Wide compatibility with standard ERC-20 | | Recipient Options | Limited | Configurable yield recipients | | Administration | Governance only | Governance + Delegated admins | ## Core Concepts ### Dual Mode: Earning vs. Non-Earning The wM token operates in two distinct modes for each account: #### Non-Earning Mode - Default state for new accounts - Balances remain static (standard ERC-20 behavior) - No yield accrues on these tokens - Simpler accounting with direct token amounts #### Earning Mode - Must be explicitly activated (requires approval via Governance or EarnerManager admin designation) - Balances remain static in storage - Represents a growing value based on a **principal amount** and an **index**, using an accounting model analogous to the native `$M` token. - Crucially, the **index used is derived from the underlying `$M` token** (wM does not maintain its own independent index). - Yield accumulates based on this principal and derived index, and can be claimed separately. Users can switch between Non-Earning and Earning modes if they meet the requirements for earning. ### Account Status Each wM account maintains several key properties tracked internally within the `WrappedMToken` contract: - **Balance**: The current wM token balance held by the account. This value remains static between operations and *excludes* any unclaimed yield. - **Earning Status**: A boolean flag indicating whether the account is currently in Earning Mode (true) or Non-Earning Mode (false). - **Earning Principal**: For accounts in Earning Mode, this stores the base principal amount used for yield calculations. This value is derived from the account's balance when it starts earning and remains constant while earning. - **Claim Recipient**: An optional address specified by the user via `setClaimRecipient()` to receive their claimed yield. If not set by the user, and no Governance override exists, yield is sent to the account owner itself. - **Earner Details**: A boolean flag indicating if settings apply when an account's earning status is managed by an EarnerManager admin. - If true, it implies the EarnerManager contract holds details (managing admin address, fee rate) for this account. - When a fee rate is set by the admin, a portion of the claimed yield (calculated as yield \* feeRate / 10000) is sent to that admin upon claiming, and the remaining net yield goes to the designated claim recipient (user-set, governance-set, or default owner). **Principal, Balance, and Yield Relationship:** When an account enters Earning Mode (`startEarningFor`), its current balance is converted into its initial `earningPrincipal`. This conversion is performed by the `IndexingMath.getPrincipalAmountRoundedDown`(balance, currentIndex) function, which calculates the principal amount corresponding to the balance at the currentIndex, taking into account the system's fixed-point scaling for the index and applying rounding rules that slightly favor the protocol. This `earningPrincipal` remains fixed while the account continues in Earning Mode. As the global `currentIndex` (derived from the `$M` token's index) increases over time, the value represented by this fixed principal grows. The fundamental relationship is: [[[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[Value Represented]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[earningPrincipal]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[currentIndex]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} The account's stored balance only increases when yield is explicitly claimed. Therefore, the `accruedYield` represents the difference between the current value represented by the principal and the static stored balance: [[[]{.katex-mathml}[[[]{.strut style="height:0.6944em;"}[[accruedYield]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[earningPrincipal]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[[currentIndex]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[balance]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} So, the target state before claiming is effectively: [[[]{.katex-mathml}[[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[[balance]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[accruedYield]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[≈]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[earningPrincipal]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[currentIndex]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} Calling the `claimFor()` function calculates this `accruedYield`. It then handles potential fee deductions if `hasEarnerDetails` is true (sending the fee portion to the managing admin). The remaining net yield is transferred to the designated `claimRecipient`. If the `claimRecipient` is the account owner itself, their stored balance is increased by this net yield amount. This operation "catches up" the account's stored balance to reflect the value earned up to that point (minus any fees paid). ### Yield Generation and Claiming #### How Yield Accrues For accounts in earning mode, yield accrues based on: - The account's principal amount - The global wM index (derived from the `$M` token's index) - The time elapsed since the last index update The formula is: [[[]{.katex-mathml}[[[]{.strut style="height:0.6944em;"}[a]{.mord.mathnormal}[ccr]{.mord.mathnormal style="margin-right:0.0278em;"}[u]{.mord.mathnormal}[e]{.mord.mathnormal}[d]{.mord.mathnormal}[Y]{.mord.mathnormal style="margin-right:0.2222em;"}[i]{.mord.mathnormal}[e]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[d]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[(]{.mopen}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[c]{.mord.mathnormal}[i]{.mord.mathnormal}[p]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[c]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}[)]{.mclose}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[ba]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[an]{.mord.mathnormal}[ce]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} Where: - `principal` is the stored earning principal for the account - `currentIndex` is the current wM index - `balance` is the current token balance #### Claiming Process When yield is claimed: 1. The accrued yield is calculated based on the current index 2. The account's balance is provisionally increased by the full yield amount 3. If the account's earning status is managed by an active EarnerManager admin with a configured fee rate, a percentage of the yield is deducted and sent to that admin as a fee 4. The remaining yield (minus any fees) is transferred to the designated recipient (or remains with the account owner if they are the recipient) Claiming doesn't change the principal amount, only the final balance reflects the net yield received. ### Index Mechanism Rather than maintaining its own independent index, wM derives its index from the `$M` token's index: #### When Earning is Enabled The wM index is calculated as: [[[]{.katex-mathml}[[[]{.strut style="height:0.8333em;vertical-align:-0.15em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[in]{.mord.mathnormal.mtight}[d]{.mord.mathnormal.mtight}[e]{.mord.mathnormal.mtight}[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[(]{.mopen}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[c]{.mord.mathnormal.mtight}[u]{.mord.mathnormal.mtight}[r]{.mord.mathnormal.mtight style="margin-right:0.0278em;"}[r]{.mord.mathnormal.mtight style="margin-right:0.0278em;"}[e]{.mord.mathnormal.mtight}[n]{.mord.mathnormal.mtight}[t]{.mord.mathnormal.mtight}[in]{.mord.mathnormal.mtight}[d]{.mord.mathnormal.mtight}[e]{.mord.mathnormal.mtight}[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[[ ]{.mord}]{.mord.text}[/]{.mord}[[ ]{.mord}]{.mord.text}[e]{.mord.mathnormal}[nab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[M]{.mord.mathnormal style="margin-right:0.109em;"}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}[)]{.mclose}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} Where: - `M current index` is the current index from the `$M` token - `enableMIndex` is the `$M` index when earning was enabled - `disableIndex` is the previous wM index (if earning was previously disabled) #### When Earning is Disabled The wM index becomes static: [[[]{.katex-mathml}[[[]{.strut style="height:0.8333em;vertical-align:-0.15em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[in]{.mord.mathnormal.mtight}[d]{.mord.mathnormal.mtight}[e]{.mord.mathnormal.mtight}[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.6944em;"}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} This approach ensures that wM's yield calculation accurately reflects the underlying `$M` token's yield performance. ### Yield Recipients The wM system provides flexible options for directing claimed yield: #### Default Behavior By default, yield is claimed to the same account that generated it. #### Custom Recipients Account holders can specify a different address to receive their yield by calling `setClaimRecipient()`. #### Governance Overrides The TTG governance can set override recipients for specific accounts through the Registrar. #### Priority Order When determining where to send claimed yield: 1. User-specified recipient (if set) 2. Governance-specified override (if set) 3. The account itself (default) This flexibility allows for a variety of yield utilization strategies. ### System Invariants and Excess The wM system is designed around a critical solvency invariant to ensure it's always fully backed by the underlying `$M` token. **Balance Invariant**: The total `$M` token balance held by the `WrappedMToken` contract must always be greater than or equal to the sum of all wM tokens in circulation plus all accrued yield owed to earners. Expressed conceptually: [[[]{.katex-mathml}[[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[≥]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[Total wM Supply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[Total Accrued Yield]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} Expressed using contract variables: [[[]{.katex-mathml}[[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[≥]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.223em;vertical-align:-1.5285em;"}[[[[[[]{.pstrut style="height:3em;"}[[[[Total wM Liabilities]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-1.4715em;"}[[]{.pstrut style="height:3em;"}[[[[[[]{.pstrut style="height:3em;"}[[]{.brace-left style="height:0.548em;"}[]{.brace-center style="height:0.548em;"}[]{.brace-right style="height:0.548em;"}]{.stretchy style="height:0.548em;min-width:1.6em;"}]{.svg-align style="top:-2.1576em;"}[[]{.pstrut style="height:3em;"}[[[totalNonEarningSupply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[projectedEarningSupply]{.mord}]{.mord.text}]{.mord}]{style="top:-3em;"}]{.vlist style="height:0.6944em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.8424em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{style="top:-3em;"}]{.vlist style="height:0.6944em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.5285em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} **How the Invariant is Maintained:** Crucially, the `M Balance` held by the `WrappedMToken` contract is not static. Since the `WrappedMToken` contract itself is registered as an `$M` earner (as shown in the `$M` dashboard where the wM contract is the largest holder), **the underlying `$M` tokens it holds continuously accrue yield according to the `$M` token's rebasing mechanism.** This intrinsic yield generation on the *entire* pool of `$M` tokens held within the wM contract is fundamental to maintaining the solvency invariant. It ensures the `$M` balance grows naturally over time, actively keeping assets (M held) ahead of liabilities (wM supply + accrued yield). **Excess Reserves:** The difference between the actual `M Balance` held by the contract and the total calculated liabilities (`totalNonEarningSupply + projectedEarningSupply`) constitutes the **"excess"** reserves within the system. This excess primarily accumulates from two sources explained further below: 1. The yield generated by the underlying `$M` tokens that back the balances of **non-earning** wM holders (as this yield isn't owed back to them). 2. Minor positive rounding effects during various token operations (transfers, wrapping/unwrapping, starting/stopping earning). This invariant and the resulting excess ensure that the system remains fully solvent, can always honor unwrap requests, and captures additional value for the protocol ecosystem. ## Features and Operations ### Basic Token Operations #### Wrapping `$M` to wM Users can deposit `$M` tokens to receive an equivalent amount of wM: ```solidity wrap(recipient, amount) ``` The `$M` tokens are held by the wM contract and can potentially earn yield. #### Unwrapping wM to M Users can burn wM tokens to receive the equivalent amount of M: ```solidity unwrap(recipient, amount) ``` Any accrued yield remains in the user's wM balance if they're in earning mode. #### Gasless Operations The contract supports wrapping with permits for improved UX: ```solidity wrapWithPermit(recipient, amount, deadline, signature) ``` #### Standard ERC-20 Operations All standard ERC-20 functions are supported, including: - `transfer(to, amount)` - `transferFrom(from, to, amount)` - `approve(spender, amount)` - `balanceOf(account)` ### Earning Management #### Starting to Earn Approved accounts can activate earning status: ```solidity startEarningFor(account) ``` This converts their balance to a principal amount that will begin accruing yield. #### Stopping Earning Accounts can deactivate earning status (or anyone can do this for an account that's no longer approved): ```solidity stopEarningFor(account) ``` This claims any outstanding yield and converts the balance back to non-earning. #### Global Earning Status The wM contract itself must be an approved earner in the `$M` token system. Earning can be enabled or disabled for the entire system: ```solidity enableEarning() disableEarning() ``` A key improvement in wM v2 is the ability to re-enable earning after it has been disabled. ### Yield Management #### Claiming Yield Accrued yield can be claimed at any time: ```solidity claimFor(account) ``` This increases the account's wM balance by the yielded amount and triggers appropriate transfers based on recipient settings. #### Setting Claim Recipients Users can direct their yield to another address: ```solidity setClaimRecipient(recipient) ``` This allows for flexible yield strategies without changing the underlying balance. #### Checking Accrued Yield Several view functions allow users and integrations to check the yield that has accrued for an earning account but has **not yet been claimed**: - `accruedYieldOf(account)`: - Returns the amount of wM tokens earned as yield based on the account's `earningPrincipal` and the time elapsed (represented by the increase in the `currentIndex`) **since the last claim or since earning started**. - This represents **only the unclaimed portion** of the yield. It does *not* include yield that has already been claimed and added to the balance. - If the account is not in Earning Mode, this returns 0. - `balanceWithYieldOf(account)`: - Returns the sum of the account's current stored `balanceOf` **plus** the currently claimable `accruedYieldOf`. - This function effectively shows the total wM value attributable to the account *at that moment*, representing what the balance *would be* if the accrued yield were claimed instantly (before considering any fees or alternate claim recipients). **Effect of Claiming:** When `claimFor(account)` is successfully executed, the calculated accrued yield is processed. If the yield (net of fees) is directed to the account owner, their stored `balanceOf` increases. Consequently, immediately after a claim, `accruedYieldOf` will return 0 (or a negligible amount due to block timing), as the yield is no longer "accrued but unclaimed". `balanceWithYieldOf` will then equal `balanceOf`. New yield will start accumulating again as time passes and the `currentIndex` increases further. ### Excess Management & Value Capture **Concept:** The wM contract holds underlying `$M` tokens. Because the wM contract itself earns yield on *all* these `$M` tokens, but wM yield is only accrued for *earning-mode* wM holders, a surplus (or "excess") of `$M` tokens naturally accumulates within the contract over time. This primarily comes from the `$M` yield earned on tokens backing non-earning wM balances. ::note M and WrappedM are sharing the same Earners list. An earner of `$M` is an earner of WrappedM. :: **Claiming Excess:** This accumulated excess M, representing value captured by the protocol, can be claimed and transferred out using the `claimExcess()` function. ```solidity claimExcess() returns (uint240 claimed_) ``` **Destination:** The excessDestination address, set during deployment, receives these claimed tokens. Typically, this is the **Distribution Vault**, directing this value back to the protocol ecosystem and ultimately benefiting Zero token holders. ### Admin Operations via EarnerManager The EarnerManager contract empowers designated admins ('special users') with the delegated authority to approve wM earners, managing status for potentially many accounts outside the standard governance process. #### Setting Earner Status Admins can enable accounts for earning and set fee rates: ```text setEarnerDetails(account, status, feeRate) ``` #### Bulk Operations Multiple accounts can be managed in a single transaction: ```text setEarnerDetails(accounts[], statuses[], feeRates[]) ``` #### Checking Earner Status Various functions provide information about earner status: ```text earnerStatusFor(account) getEarnerDetails(account) ``` ## Technical Design ### Account Model The wM contract uses a sophisticated account model to efficiently track balances and yield information: ```solidity struct Account { bool isEarning; // Whether the account is in earning mode uint240 balance; // Current token balance (excluding yield) uint112 earningPrincipal; // Principal amount for earning accounts bool hasClaimRecipient; // Whether a custom recipient is set bool hasEarnerDetails; // Whether admin fee settings exist } ``` This structure packs related data together to minimize storage costs while maintaining all necessary information for the dual-mode system. ### Principal-Based Accounting A key aspect of wM's design is its principal-based accounting system for earning accounts: #### Principal Conversions When an account starts earning, its balance is converted to a principal amount: ```text principal = balance / currentIndex (rounded down) ``` This principal remains constant while the account is earning, while its value in tokens grows with the index. #### Present Value Calculation The current token value of a principal is: ```text presentValue = principal * currentIndex ``` This calculation is used when checking balances and claiming yield. #### Rounding Strategy The contract employs consistent rounding rules: - When converting from tokens to principal (adding to earning), principal is rounded DOWN - When calculating present value (for balance displays), the value is exact - When subtracting from earning accounts, principal is rounded UP These rules slightly favor the protocol, creating a small buffer of excess tokens that enhances stability. ### Supply Tracking The contract maintains precise tracking of token supply: - `totalNonEarningSupply`: Sum of all non-earning account balances - `totalEarningSupply`: Sum of all earning account balances (excluding accrued yield) - `totalEarningPrincipal`: Sum of all earning account principals - Total supply = `totalNonEarningSupply + totalEarningSupply` - `projectedEarningSupply`: The total earning supply if all accrued yield was claimed These values are used for various calculations, including excess determination. ### Transfer Mechanics The transfer system handles different scenarios based on account earning status: #### Same Status Transfers - Between earning accounts: Converts the amount to principal using the current index - Between non-earning accounts: Simple balance transfers without conversion #### Cross-Status Transfers - From earning to non-earning: Converts from principal to tokens - From non-earning to earning: Converts from tokens to principal - Affects total earning and non-earning supply tracking Each transfer type ensures that exact token amounts move between accounts while maintaining proper accounting in the appropriate mode. ### Fee Mechanism When enabled by an admin through the EarnerManager, fees can be taken from yield: - Fees are a percentage (in basis points, max 10,000) of claimed yield - Fee amounts are transferred to the admin who approved the account - If an admin is removed from the admin list, their fee settings become invalid - Fees are processed during the claim operation This creates an incentive for admins to onboard and manage accounts in the system. ### Excess Mechanism The `WrappedMToken` (wM) contract is designed to naturally accumulate "excess" `$M` tokens over time. This excess represents the `$M` tokens held by the contract *above and beyond* what is required to fully back all circulating wM tokens and their accrued yield entitlements. This mechanism enhances system stability and creates a value capture opportunity for the protocol. **Core Principle: Yield Mismatch** The fundamental driver of excess accumulation stems from a mismatch in yield earning: 1. **wM Contract Earns `$M` Yield:** As confirmed by its status as a major `$M` holder and its requirement to be an approved M earner (`enableEarning` requires `_isThisApprovedEarner`), the **entire balance of `$M` tokens held within the `WrappedMToken` contract continuously earns yield** according to the `$M` token's native rebasing mechanism. 2. **Not All wM Holders Earn wM Yield:** However, only wM holders whose accounts are explicitly set to **Earning Mode** (via Governance or EarnerManager approval) accrue a corresponding claimable wM yield entitlement based on their principal. Holders in **Non-Earning Mode** do not accrue wM yield. **Sources of Excess Accumulation:** The excess `$M` balance primarily grows from: 1. **Yield on `$M` Backing Non-Earning wM (Primary Source):** The `$M` yield generated by the portion of `$M` tokens backing the wM held by users in Non-Earning Mode directly contributes to the excess. Since these users aren't entitled to wM yield, the `$M` yield earned on their underlying share remains within the contract as surplus. 2. **Rounding Effects during Principal Conversions (Secondary Source & Potential Deficit):** The contract utilizes principal-based accounting for earners, requiring conversions between token amounts and principal values whenever balances change (e.g., `wrap`, `unwrap`, `startEarningFor`, `stopEarningFor`, cross-status transfers). 3. **Direct `$M` Transfers:** Any `$M` tokens sent directly to the `WrappedMToken` contract address (outside the `wrap` function) also become part of the excess. 4. **Cross-Status Transfers:** The mechanics of transfers between earning and non-earning accounts can also contribute minor gains due to the specific rounding applied during the principal conversions involved. **Calculating Excess:** The contract calculates the available excess using the `excess()` view function: [[[]{.katex-mathml}[[[]{.strut style="height:1.0385em;vertical-align:-0.3552em;"}[[[Excess]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:2.3341em;vertical-align:-1.5841em;"}[[[[[[]{.pstrut style="height:3em;"}[[[[Total wM Liabilities]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-1.4159em;"}[[]{.pstrut style="height:3em;"}[[[[[[]{.pstrut style="height:3em;"}[[]{.brace-left style="height:0.548em;"}[]{.brace-center style="height:0.548em;"}[]{.brace-right style="height:0.548em;"}]{.stretchy style="height:0.548em;min-width:1.6em;"}]{.svg-align style="top:-2.102em;"}[[]{.pstrut style="height:3em;"}[[[(totalNonEarningSupply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[ProjectedEarningSupply)]{.mord}]{.mord.text}]{.mord}]{style="top:-3em;"}]{.vlist style="height:0.75em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.898em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{style="top:-3em;"}]{.vlist style="height:0.75em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.5841em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} **Excess Management:** The accumulated excess `$M` tokens can be claimed and transferred out of the contract via the `claimExcess()` function. The `excessDestination` is set during deployment and typically points to the **Distribution Vault**, creating a revenue stream for the broader protocol ecosystem, ultimately benefiting Zero token holders. ## Integration Considerations ### For DeFi Protocols When integrating with wM, protocols should be aware of: - Standard ERC-20 behavior (no rebasing) - Distinction between balance and balance-with-yield - Impact of earning status on user experience - Potential for yield to accumulate while tokens are locked ### For Developers Developers working with wM should understand: - The relationship between wM and the underlying `$M` token - The impact of earning status on token accounting - The claiming process and its effect on balances - The consistent rounding patterns used throughout the contract ### Common Patterns Several patterns are recommended when working with wM: - Check both `balanceOf()` and `balanceWithYieldOf()` to understand a user's position - Consider calling `claimFor()` before critical operations to ensure balances reflect all value - Be aware of the impact of earning status transitions on token amounts - Understand the priority order for claim recipients when handling claimed yield ## Security Considerations ### Trust Model The wM system operates with several trust assumptions: - **MigrationAdmin**: Fully trusted role that can change contract implementation - **EarnerManager MigrationAdmin**: Can change EarnerManager implementation - **Registrar**: Source of truth for system parameters - **MToken**: Assumed to function correctly - **Admins**: Trusted to manage earning status appropriately ### Edge Cases Several edge cases are worth understanding: #### Admin Removal If an admin is removed from the admins list: - Accounts they approved lose earning status - Their fee settings become invalid - Accounts should be stopped from earning to prevent further yield accrual #### Disabled Earning If the wrapper contract is removed from M's earner list: - `disableEarning()` should be called to prevent further yield accrual - Individual accounts can continue to earn until that point #### Balance Precision The system has specific precision limitations: - All balances limited to uint240 to match `$M` token - Principal amounts limited to uint112 for gas efficiency - Indices use uint128 with 12 decimal places of precision ### Rounding Effects The consistent rounding strategy creates specific effects: - Multiple conversions between earning and non-earning states will result in small token losses - Transfers between accounts with different earning statuses create small protocol gains - These tiny amounts accumulate as protocol reserves, enhancing stability ## Upgrades and Migration ### Key Improvements in V2 Wrapped `$M` (wM) fundamentally operates as a non-rebasing token where yield is realized through manual claiming, a characteristic distinct from the native `$M` token's automatic rebasing. The V2 implementation builds upon this established model with several important enhancements: 1. **Re-enabling Earning**: V2 allows earning to be re-enabled after being disabled 2. **Admin-Managed Earners**: Introduction of EarnerManager for delegated administration 3. **Fee Mechanism**: Ability for admins to take fees from yield 4. **Principal-Based Accounting**: Changed from lastIndex tracking to principal amount tracking 5. **Explicit Claim Recipients**: Added ability for users to explicitly set yield recipients 6. **Gas Efficiency**: Optimized storage layout and operations ### Migration from V1 The migration from V1 to V2 involved several key transformations: #### Account Model Change - V1: Used last interaction index for earning accounts - V2: Uses principal amount for earning accounts - Migration formula: `principal = balance / lastIndex` #### Index Handling - V1: Used an array for enable/disable indices - V2: Uses enableMIndex and disableIndex slots - Migration preserved earning state and index values #### Migration Process The migration was handled through a specialized migrator contract that: 1. Identified all earning accounts in V1 2. Converted their account structure to the V2 format 3. Preserved their earning status and equivalent yield position # Wrapped M Specification ## Contract overview `WrappedMToken` (wM) is a non-rebasing ERC-20 wrapper for the rebasing `$M` token. It maintains yield-earning capabilities while providing compatibility with DeFi protocols that require standard ERC-20 tokens. - **Decimals:** 6 (matches `$M`) - **Index:** Derived from the underlying `$M` token's index - **Yield realization:** Explicit claiming (not automatic rebasing) --- ## Key functions ### Wrapping and Unwrapping | Function | Description | | ---------------------------------------------------------------------------------------------------- | ------------------------------------------ | | `wrap(address recipient, uint256 amount)` | Deposit `$M` tokens, receive equivalent wM | | `unwrap(address recipient, uint256 amount)` | Burn wM tokens, receive equivalent `$M` | | `wrapWithPermit(address recipient, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)` | Wrap with gasless `$M` approval | ### ERC20 Standard | Function | Description | | -------------------------------------------------------- | ------------------------------------------------- | | `transfer(address to, uint256 amount)` | Transfer wM tokens | | `transferFrom(address from, address to, uint256 amount)` | Transfer wM on behalf of another address | | `approve(address spender, uint256 amount)` | Approve a spender allowance | | `balanceOf(address account)` | Returns stored balance (excludes unclaimed yield) | | `totalSupply()` | Returns total wM in circulation | ### Earning Management | Function | Description | | ---------------------------------- | -------------------------------------------------- | | `startEarningFor(address account)` | Activate earning mode for an approved account | | `stopEarningFor(address account)` | Deactivate earning mode (claims outstanding yield) | | `enableEarning()` | Enable earning for the wM contract itself on `$M` | | `disableEarning()` | Disable earning for the wM contract on `$M` | ### Yield Management | Function | Description | | -------------------------------------- | --------------------------------------------- | | `claimFor(address account)` | Claim accrued yield for an account | | `setClaimRecipient(address recipient)` | Set a custom address to receive claimed yield | ### Balance and Yield Queries | Function | Returns | | ------------------------------------- | ------------------------------------------------ | | `balanceOf(address account)` | Stored balance (excludes unclaimed yield) | | `accruedYieldOf(address account)` | Unclaimed yield for an earning account | | `balanceWithYieldOf(address account)` | `balanceOf` + `accruedYieldOf` | | `isEarning(address account)` | Whether the account is in earning mode | | `totalNonEarningSupply()` | Sum of all non-earning balances | | `totalEarningSupply()` | Sum of all earning balances (excluding yield) | | `projectedEarningSupply()` | Total earning supply if all yield were claimed | | `excess()` | `$M` held by contract minus total wM liabilities | ### Excess Management | Function | Description | | --------------------- | ------------------------------------------------------------------------------- | | `claimExcess()` | Transfer accumulated excess `$M` to `excessDestination` | | `excessDestination()` | Returns the address that receives claimed excess (typically Distribution Vault) | ### Admin Operations (via EarnerManager) | Function | Description | | -------------------------------------------------------------------------- | ---------------------------------------------------- | | `setEarnerDetails(address account, bool status, uint16 feeRate)` | Set earning status and fee rate for an account | | `setEarnerDetails(address[] accounts, bool[] statuses, uint16[] feeRates)` | Bulk set earning details | | `earnerStatusFor(address account)` | Check earner status for an account | | `getEarnerDetails(address account)` | Get full earner details including admin and fee rate | --- ## Account storage model ```solidity struct Account { bool isEarning; // Whether the account is in earning mode uint240 balance; // Current token balance (excluding yield) uint112 earningPrincipal; // Principal amount for earning accounts bool hasClaimRecipient; // Whether a custom recipient is set bool hasEarnerDetails; // Whether admin fee settings exist } ``` --- ## Events | Event | Description | | ---------------------------------------------------------------------------- | ---------------------------------- | | `Transfer(address indexed from, address indexed to, uint256 value)` | Standard ERC20 transfer | | `Approval(address indexed owner, address indexed spender, uint256 value)` | Standard ERC20 approval | | `StartedEarning(address indexed account)` | Account entered earning mode | | `StoppedEarning(address indexed account)` | Account exited earning mode | | `Claimed(address indexed account, address indexed recipient, uint256 yield)` | Yield claimed | | `ExcessClaimed(uint240 amount)` | Excess `$M` claimed to destination | --- ## Errors | Error | Condition | | ----------------------------------------------------------------------- | ------------------------------------------------ | | `NotApprovedEarner()` | Account is not approved for earning | | `EarningIsEnabled()` | Operation not permitted while earning is enabled | | `EarningIsDisabled()` | Operation requires earning to be enabled | | `InsufficientBalance(address account, uint256 balance, uint256 amount)` | Transfer, unwrap, or burn exceeds balance | --- ## Index mechanism The wM index is derived from the underlying `$M` token's index: **When earning is enabled:** [[[]{.katex-mathml}[[[]{.strut style="height:0.8333em;vertical-align:-0.15em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[[index]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.0463em;vertical-align:-0.686em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[e]{.mord.mathnormal}[nab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[M]{.mord.mathnormal style="margin-right:0.109em;"}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.mord}]{style="top:-2.314em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[[currentIndex]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}]{.mord}]{style="top:-3.677em;"}]{.vlist style="height:1.3603em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.686em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} **When earning is disabled:** [[[]{.katex-mathml}[[[]{.strut style="height:0.8333em;vertical-align:-0.15em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[[index]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.6944em;"}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} --- ## Yield claim recipient priority When determining where to send claimed yield: 1. User-specified recipient (via `setClaimRecipient`) 2. Governance-specified override (via Registrar) 3. The account itself (default) --- ## Solvency invariant [[[]{.katex-mathml}[[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[≥]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[totalNonEarningSupply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[projectedEarningSupply]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} The excess above this threshold accumulates from yield on `$M` backing non-earning wM holders and minor rounding effects. --- ## Related - [Wrapped M overview](https://docs.m0.org/protocol/wrapped-m) - Full conceptual documentation - [M Token specification](https://docs.m0.org/protocol/m-token-spec) - `$M` token spec - [Distribution Vault](https://docs.m0.org/protocol/distribution-vault) - Where excess is typically directed # PYUSDx Overview PYUSDx is a developer platform built on M0 infrastructure. It allows partners to program onchain products anchored around PYUSD. Partners don't offer PYUSDx to users directly. Instead, they create a **custom token** on top of it: their own name and symbol, their own behavior, backed 1:1 by PYUSDx, with rewards routed to a treasury they choose. M0 operates the shared layer: the developer platform, the swap infrastructure, the cross-chain bridge, and rewards enablement for each custom token. This page explains the model. The [Quick Guide](https://docs.m0.org/pyusdx/quick-guide) describes the launch sequence, and the [technical documentation](https://docs.m0.org/protocol/pyusdx/overview) covers each contract in depth. ::note This page uses the term *custom token*. The contracts and the technical documentation call the same thing an *extension*. The terms are interchangeable. :: ## Key facts | Property | Detail | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Developer platform** | PYUSDx is an upgradeable, non-rebasing ERC-20 with 6 decimals, matching PYUSD. | | **Rewards** | Accrues continuously on the PYUSDx that backs your custom token. Rewards are added to a balance only when claimed, so holder balances don't change on their own. | | **Custom token templates** | `YieldToOne` routes all rewards to one treasury. `MultiMint` does the same and also accepts approved alternative stablecoins as backing. | | **Wrap and unwrap** | 1:1 through `SwapFacility`. No spread and no liquidity pool to fund. | | **Cross-chain** | `Portal` uses burn-and-mint with pluggable bridge adapters. LayerZero is the first adapter. | | **Supported chains** | Ethereum, Arbitrum, Monad, and Base. Testnets: Ethereum Sepolia, Arbitrum Sepolia, Monad Testnet, and Base Sepolia. | | **Compliance controls** | Account freezing, forced transfer from frozen accounts, and pausing. | | **Upgrade model** | Core contracts are deployed behind transparent proxies that M0 operates. Custom tokens are deployed behind beacon proxies: they follow M0's latest audited implementation by default, and you can pin a version. | | **Mainnet launch** | June 2, 2026, on Ethereum and Arbitrum; Monad followed on August 12, 2026, and Base on September 3, 2026. | ## How it works ### PYUSDx is the developer platform Every custom token is powered 1:1 by PYUSDx, the common infrastructure that all partners share. Because the base layer is shared, conversions are exact and liquidity is available immediately. You don't need to bootstrap a separate market for your token. ### You launch a custom token Your token is a wrapper around PYUSDx, deployed in a single transaction through M0's `ExtensionFactory`. Two audited templates are available: - `YieldToOne` routes all rewards to a single treasury address. M0 recommends this template for most partners. - `MultiMint` routes rewards the same way and also accepts approved alternative stablecoins as backing, each under a per-asset cap. Unwrapping always returns PYUSDx. You choose the name, symbol, rewards recipient, and the address that holds each administrative role. For details, see [Scope with M0](https://docs.m0.org/pyusdx/quick-guide#_1-scope-with-m0) in the Quick Guide. ### Users wrap and unwrap at 1:1 `SwapFacility` is the only entry and exit point for every custom token. `swapIn` converts PYUSDx into your token, `swapOut` converts it back, and `swapExtensions` converts directly between two custom tokens. Amounts are exact: the custom tokens minted or the PYUSDx returned always equal the amount supplied. Permit-based approvals support gasless swaps. ### The backing earns rewards while holder balances stay fixed PYUSDx is non-rebasing. A balance never changes on its own; rewards accrue in the background and are added only when claimed. Your custom token's contract is registered as an earner on PYUSDx, so the PYUSDx it holds accrues rewards continuously. When your yield manager calls `claimYield`, the accrued amount (the contract's PYUSDx balance minus your token's total supply) is minted as new custom tokens to your rewards recipient. Holder balances don't change. The recipient receives the claimed rewards. ### Tokens move across chains `Portal` bridges PYUSDx and custom tokens between supported chains using burn-and-mint: tokens are burned on the source chain and minted on the destination chain, so total supply stays consistent across chains. When a user bridges a custom token, `Portal` unwraps it to PYUSDx, sends the message, and wraps it again on arrival. Bridge providers are abstracted behind an adapter interface. LayerZero is the first adapter, and M0 can add others without changing `Portal`. ### Compliance controls are built in Your custom token inherits freeze and pause controls, held by roles you assign. Frozen accounts can't send, receive, or claim rewards. A pause stops all transfers. Forced transfers, which move funds out of a frozen account under legal process, operate at the PYUSDx layer. Freezing an earning account claims its pending rewards and stops earning first, so the full balance is available for a forced transfer. ## Architecture | Contract | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------- | | `PYUSDX` | Core ERC-20 with per-account rewards. | | `IssuerGateway` | Time-delayed mint and burn gateway for PYUSDx supply. M0 operates it. | | `SwapFacility` | 1:1 router between PYUSDx and custom tokens. M0 operates it. | | `ExtensionFactory` | Deploys and registers custom tokens at deterministic addresses. M0 operates it; you call it once per chain. | | `ExtensionBeacon` | Per-template registry of audited implementations. M0 operates it. | | `YieldToOne` / `MultiMint` | Your custom token. You hold its roles. | | `Portal` and adapters | Cross-chain bridge and provider adapters. M0 operates them. | ## Integration notes - **Balances are stable.** Custom tokens behave like any ERC-20. No rebasing math is required. - **Rewards appear as minted supply at claim time.** Your token's total supply grows by the claimed amount and the contract emits `YieldClaimed`. Base treasury reporting on claim events, not on block time. - **Addresses are deterministic.** The same deployer and the same deployment name produce the same token address on every supported chain. - **Decimals are 6.** PYUSDx and every custom token use 6 decimals, matching PYUSD. ## What's next ::card-group :::card --- icon: i-lucide-list-checks title: Quick Guide to: https://docs.m0.org/pyusdx/quick-guide --- The launch sequence: each step, its owner, and its result. ::: :::card --- icon: i-lucide-book-open title: Technical documentation to: https://docs.m0.org/protocol/pyusdx/overview --- The engineering reference: token, rewards, issuance, extensions, bridging, and security. ::: :::card --- icon: i-lucide-file-text title: Specification to: https://docs.m0.org/protocol/pyusdx-spec --- Lookup tables: parameters, roles, rounding, errors, and events. ::: :: # PYUSDx Quick Guide Launching a custom token requires a set of decisions, one deploy transaction, and a handoff to M0. M0 operates the shared infrastructure that enables rewards for your token. This page maps the full sequence so you can scope the work. For the commands, see the [deployment guide](https://docs.m0.org/build/pyusdx/deploy-extension). ## Before you begin You need the following: - **A deployer wallet** with gas on each target chain. Start on Ethereum Sepolia. - **An admin address,** preferably a multisig, to hold `DEFAULT_ADMIN_ROLE` on your token, and addresses for the operational roles in step 2. On testnet, one wallet can hold every role. - **A treasury address** to receive rewards. - **Testnet PYUSDx;** faucets are deployed on Ethereum Sepolia and Arbitrum Sepolia. Addresses are on the [deployments page](https://docs.m0.org/resources/addresses/pyusdx-platform). - **An initial call with M0.** [Contact M0](https://www.m0.org/contact-us){rel=""nofollow""} before you deploy. ## Launch sequence | # | Step | Owner | Description | Result | | - | ------------------ | ------------------- | -------------------------------------------------------------- | ------------------------- | | 1 | Scope with M0 | You and M0 | Confirm the template, chains, timeline, and rewards enablement | Completed decisions table | | 2 | Assign roles | You | Choose an address for each role on your token | Role assignments | | 3 | Deploy on testnet | You (M0 can assist) | One call to `ExtensionFactory` on Ethereum Sepolia | Token address | | 4 | Test your token | You | Wrap, unwrap, bridge, freeze, and pause | Test checklist complete | | 5 | Share the address | You | Send your token address and chain to M0 | Handoff complete | | 6 | Onboard | MoonPay & You | Complete MoonPay's KYB process | Onboarded for rewards | | 7 | Enable rewards | MoonPay | MoonPay registers your token as an earner on PYUSDx | Rewards accruing | | 8 | Deploy on mainnet | You (M0 can assist) | Repeat steps 3, 5, and 6 on each target chain | Token live | | 9 | Operate your token | You | Claim rewards, manage roles, apply controls, manage upgrades | Ongoing | ### 1. Scope with M0 A short set of decisions defines your token. | Decision | Options | Notes | | --------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Template** | `YieldToOne` or `MultiMint` | `YieldToOne` routes all rewards to one treasury. M0 recommends it for most partners. `MultiMint` adds approved alternative stablecoins as backing under per-asset caps and adds one more role. Fixed at deployment. | | **Name and symbol** | Your brand | For example, "Acme USD" and `aUSD`. Fixed at deployment. Distinct from the deployment name in step 3. | | **Rewards recipient** | A treasury address | Where claimed rewards are minted. `YIELD_RECIPIENT_MANAGER_ROLE` can change it later. | | **Role holders** | One address per role | See step 2. `DEFAULT_ADMIN_ROLE` can change them later. | | **Chains** | One or more supported chains | Ethereum, Arbitrum, Monad, and Base. Start with one chain and add more later. Reusing the same deployer and deployment name produces the same address on each chain. | | **Compliance policy** | Your policy | How and when you apply freeze and pause, and how you coordinate with M0 on forced transfers. | **Fixed at deployment:** template, name, and symbol. **Changeable later:** rewards recipient, role holders, and the set of chains. ### 2. Assign roles Each role on your token is independent. On testnet, assign one wallet to every role. For mainnet, separate them based on which wallets own each role. | Role | Permissions | Suggested holder | | ----------------------------------------- | ------------------------------------------------------------------- | ------------------- | | `DEFAULT_ADMIN_ROLE` | Grants and revokes every other role | Multisig | | `YIELD_RECIPIENT_MANAGER_ROLE` | Calls `claimYield` and sets the rewards recipient | Treasury operations | | `FREEZE_MANAGER_ROLE` | Freezes and unfreezes accounts on your token | Compliance | | `PAUSER_ROLE` | Pauses and unpauses transfers on your token | Incident response | | `VERSION_MANAGER_ROLE` | Pins your token to a specific implementation version, or unpins it | Engineering | | `ASSET_CAP_MANAGER_ROLE` (MultiMint only) | Sets per-asset caps and manages the `replaceAsset` caller allowlist | Treasury operations | ::note Forced transfers, which move funds out of a frozen account, are a PYUSDx-level control that M0 holds. Restricted addresses are expected to be frozen on PYUSDx and on every custom token, so include a coordination path with M0 in your compliance policy. :: ### 3. Deploy on testnet Call `deployYieldToOne(extensionName, params)` or `deployMultiMint(extensionName, params)` on `ExtensionFactory`. In one transaction, the factory resolves the latest audited implementation from the template's beacon, deploys your proxy at a deterministic address, registers it, and emits `ExtensionDeployed`. You can deploy from a block explorer without local tooling, or with a script. The [deployment guide](https://docs.m0.org/build/pyusdx/deploy-extension) covers both methods. ::warning{title="Caution"} `extensionName` and `name` are different parameters. `extensionName` determines your token's address. It must be unique per deployer per network, and reusing it from the same deployer on another network produces the same address there. `name` and `symbol` in `params` are the ERC-20 fields that users see. :: ### 4. Test your token Complete these checks on Ethereum Sepolia before you ask M0 to enable rewards: 1. Wrap testnet PYUSDx into your token with `SwapFacility.swapIn` and confirm that the amounts match 1:1. 2. Unwrap with `swapOut` and confirm that the full PYUSDx amount is returned. 3. Bridge a test amount with `Portal` to Arbitrum Sepolia, Monad Testnet, or Base Sepolia. All are peered with Ethereum Sepolia. 4. Freeze a test account, confirm that its transfers revert, and unfreeze it. 5. Pause your token, confirm that transfers revert, and unpause it. 6. After step 6, call `claimYield` and confirm that new tokens are minted to your rewards recipient. ### 5. Share the address Send M0 your deployed token address and the chain it is deployed on. ### 6. Onboard with MoonPay through KYB MoonPay is the regulated issuer of PYUSDx and verifies every partner that receives rewards. Before MoonPay can register your token as an earner, your entity completes MoonPay's Know Your Business (KYB) process. M0 introduces you to MoonPay once you have shared your token address. KYB confirms three things: that your company is legally registered, who owns and controls it, and how you intend to use PYUSDx. ### 7. MoonPay enables rewards MoonPay registers your token contract as an earner on PYUSDx. From that point, the PYUSDx that your contract holds accrues rewards continuously, and your yield manager can claim it with `claimYield`. Before this step, your token supports wrapping, swapping, and bridging but doesn't accrue rewards. Rewards enablement is per chain. Repeat steps 5 and 6 for each network you deploy to. ### 8. Deploy on mainnet Repeat step 3 on each target chain, followed by steps 5 and 6. On mainnet, every supported chain is peered with every other supported chain, so bridging works on every route as soon as your token is deployed on both ends. ### 9. Operate your token - **Rewards.** Call `claimYield` on a schedule that fits your treasury operations. Each call mints the accrued rewards to your recipient. Rewards never accrue to holders. To change the recipient, call `setYieldRecipient`. - **Compliance.** Apply freeze and pause according to your policy. `claimYield` succeeds while your token is paused, but the minted tokens can't transfer until you unpause. - **Upgrades.** M0 publishes new audited implementations to the template beacon. By default, your token follows the latest version automatically. Call `pinVersion` to stay on a specific version and `unpinVersion` to follow the latest version again. - **Cross-chain.** No action is required. If a bridge recipient is frozen on PYUSDx, `Portal` routes the funds to M0's fallback recipient instead of failing. - **MultiMint only.** Manage backing assets with `setAssetCap`. A cap of zero disables new deposits of that asset. Existing balances remain unwrappable to PYUSDx. ## Responsibilities | M0 operates | MoonPay operates | You control | | ------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | | Liquidity rails from your custom token | Issue and redeem PYUSDx | Every role on your token | | `SwapFacility` and shared liquidity | Maintain PYUSD collateral backing | When rewards are claimed and where it goes | | `Portal` and its bridge adapters | Manage approved partners that can access liquidity and earn rewards | How freeze and pause are applied under your policy | | The audited template implementations and their upgrades | Manage reward distribution | Whether your token follows upgrades automatically or pins a version | | Rewards enablement for your token | Oversee compliance and safeguards of PYUSDx | Backing assets and caps (MultiMint only) | You don't manage reserves, run swap infrastructure, or operate a bridge. ## Next steps ::card-group :::card --- icon: i-lucide-rocket title: Deployment guide to: https://docs.m0.org/build/pyusdx/deploy-extension --- Step-by-step instructions for deploying your token on testnet. ::: :::card --- icon: i-lucide-file-text title: Specification to: https://docs.m0.org/protocol/pyusdx-spec --- Roles, parameters, errors, and events for every contract. ::: :::card --- icon: i-lucide-mail title: Contact M0 to: https://www.m0.org/contact-us --- Start the conversation with the M0 team. ::: :: # API Reference ## Getting an API key Both Orchestration and Protocol APIs require authentication. Get in touch with the M0 team to obtain your API key. [Contact us →](https://www.m0.org/contact-us){rel=""nofollow""} --- ## Orchestration API (REST) **What it does:** Get quotes and execute conversions between M0 stablecoin extensions and supported external stablecoins (currently USDC). This is the API powering **M0's Onchain Orchestration** product. - **Endpoint**: `https://gateway.m0.xyz/v1/orchestration` - **Type**: REST - **Authentication**: API key in `x-api-key` header **Good for:** - Getting conversion quotes between your stablecoin extension and USDC - Integrating M0 liquidity into your product's flows - Automating stablecoin distribution to you, your partners or clients [Orchestration API overview →](https://docs.m0.org/api-reference/orchestration/overview) [Full endpoint reference →](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""} [Type generation →](https://docs.m0.org/api-reference/orchestration/type-generation) --- ## Protocol API (GraphQL) **What it does:** Query on-chain data - token supplies, holder balances, rewards accrual, earner status, and configuration parameters. - **Endpoint**: `https://protocol-api.m0.org/graphql` - **Type**: GraphQL - **Authentication**: API key in `Authorization` header **Good for:** - Monitoring token supply and collateral coverage - Querying rewards earned by a specific stablecoin extension - Building dashboards on top of protocol state - Auditing stablecoin extension balances and earner approvals [Try the interactive playground →](https://protocol-api.m0.org/graphql){rel=""nofollow""} [Protocol API overview →](https://docs.m0.org/api-reference/protocol/overview) [Common query recipes →](https://docs.m0.org/api-reference/protocol/recipes/network-supply) --- ## Get started ::card-group :::card --- icon: i-lucide-split title: Orchestration API to: https://docs.m0.org/api-reference/orchestration/overview --- Get quotes, place orders, and manage cross-chain operations. ::: :::card --- icon: i-lucide-key title: Protocol API to: https://docs.m0.org/api-reference/protocol/overview --- Query on-chain protocol data via GraphQL, with authentication setup and key storage best practices. ::: :::card --- icon: i-lucide-book-open title: GraphQL Recipes to: https://docs.m0.org/api-reference/protocol/recipes/network-supply --- Common GraphQL queries for network supply, token data, yields, and more. ::: :: # POST /orders/{originChain}/{orderId}/cancel This endpoint builds a cancellation transaction for an existing limit order. The returned payload must be signed and submitted to the blockchain by the caller to complete the cancellation. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Who can cancel Cancellations are initiated on the order's destination chain. Who can submit the transaction depends on whether the order is same-chain or cross-chain, and whether its `fillDeadline` has passed: | Scenario | Who can cancel | | ---------------------------------- | ----------------------------------------- | | Same-chain, before `fillDeadline` | The order's sender or recipient | | Cross-chain, before `fillDeadline` | The destination recipient | | Any order, after `fillDeadline` | Anyone, to release the unfilled remainder | For SVM payloads, the transaction is built for one signer. If the cancelling wallet is not the default signer for that scenario, include its base58 pubkey as `caller`. ## Request ### Endpoint ```text POST /orders/{originChain}/{orderId}/cancel ``` ### Headers ```text Content-Type: application/json x-api-key: YOUR_API_KEY ``` ### Path Parameters | Field | Type | Required | Description | | ------------- | -------- | -------- | -------------------------------------------------------- | | `originChain` | `Chain` | Yes | The chain where the order was created (e.g., `Ethereum`) | | `orderId` | `string` | Yes | The ID of the limit order to cancel | ### Body Parameters (optional) | Field | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `caller` | `string` | No | SVM only: override the default transaction signer with the base58 pubkey that will sign and pay for the resulting Solana transaction. Ignored for EVM destinations. | ## Example Request ```typescript import type { components } from "./m0-swap"; // From type generation type TransactionPayload = components["schemas"]["TransactionPayload"]; const originChain = "Ethereum"; const orderId = "0xabc123..."; const response = await fetch( `https://gateway.m0.xyz/v1/orchestration/orders/${originChain}/${orderId}/cancel`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, }, ); if (!response.ok) { const error = await response.text(); throw new Error(`Cancel order failed: ${error}`); } const payload: TransactionPayload = await response.json(); ``` ## Response ### Success Response (200) Returns a `TransactionPayload` to submit on-chain to complete the cancellation — either an `EvmPayload` or `SvmPayload` depending on the chain. ```typescript import type { components } from "./m0-swap"; type TransactionPayload = components["schemas"]["TransactionPayload"]; type EvmPayload = components["schemas"]["EvmPayload"]; type SvmPayload = components["schemas"]["SvmPayload"]; ``` The payload carries its own `chain` (and `chainId` for EVM payloads) indicating where the transaction must be broadcast: - **Same-chain orders** (origin == destination): broadcast on the order's chain. The payload's `chain` matches the URL `originChain`. - **Cross-chain orders** (origin != destination): the cancel must be initiated on the order's **destination chain**, which then dispatches a CancelReport message back to the origin chain to release the deposited funds. The payload's `chain` is the destination chain and **differs from the URL path parameter**. The payload's `value` includes the bridge fee. Always read `payload.chain` (and `payload.chainId` for EVM) to determine the broadcast target — do not assume it matches the URL path parameter. See the [Quote](https://docs.m0.org/api-reference/orchestration/quote) docs for payload execution examples. ### Error Responses All errors share the `{ code, message, requestId }` body shape. Branch on `code` rather than on the HTTP status. | Status | Code | Description | | ------ | --------------------- | ---------------------------------------------------------------------------- | | `400` | `BadOrderRequest` | Malformed request (unknown chain, malformed `orderId`, unsupported `caller`) | | `404` | `OrderNotFound` | The referenced order does not exist on-chain | | `409` | `OrderNotCancellable` | The order is already in a terminal state (`CANCELLED` / `COMPLETED`) | | `500` | `CancelOrderError` | Unexpected internal failure — retrying may succeed | ## Example Response ```json { "type": "evm", "chain": "Ethereum", "chainId": 1, "to": "0x1234567890abcdef...", "data": "0xabcdef...", "value": "0" } ``` ## Executing the Cancellation Once you have the response, submit the payload to the blockchain to complete the cancellation: ```typescript import { createWalletClient, http } from "viem"; import { mainnet } from "viem/chains"; import type { components } from "./m0-swap"; type EvmPayload = components["schemas"]["EvmPayload"]; async function executeCancelOrder(payload: EvmPayload, account: `0x${string}`) { const client = createWalletClient({ chain: mainnet, transport: http(), account, }); const hash = await client.sendTransaction({ to: payload.to as `0x${string}`, data: payload.data as `0x${string}`, value: BigInt(payload.value), }); return hash; } ``` ## Notes - A `409` response means the order is in a terminal state (filled or already cancelled) and no action is needed - The cancellation is not final until the returned transaction payload is submitted and confirmed on-chain # GET /orders/{originChain}/{orderId} This endpoint returns the full on-chain state plus indexer enrichment for a single order. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text GET /orders/{originChain}/{orderId} ``` ### Headers ```text x-api-key: YOUR_API_KEY ``` ### Path Parameters | Field | Type | Required | Description | | ------------- | -------- | -------- | -------------------------------------------------------- | | `originChain` | `Chain` | Yes | The chain where the order was created (e.g., `Ethereum`) | | `orderId` | `string` | Yes | The order ID to retrieve | ## Example Request ```typescript import type { components } from "./m0-swap"; type Order = components["schemas"]["Order"]; const originChain = "Ethereum"; const orderId = "0xabc123"; const response = await fetch( `https://gateway.m0.xyz/v1/orchestration/orders/${originChain}/${orderId}`, { method: "GET", headers: { "x-api-key": "YOUR_API_KEY", }, }, ); if (response.status === 404) { throw new Error("Order not found"); } if (!response.ok) { const error = await response.text(); throw new Error(`Get order failed: ${error}`); } const order: Order = await response.json(); ``` ## Response ### Success Response (200) Returns an `Order` object. On-chain fields are authoritative; indexer-only fields (`openTx`, `fillCount`, `fillReportCount`, `resolvedAt`, `fills`) are optional and may be absent if the indexer has not yet caught up or is unavailable. ```typescript import type { components } from "./m0-swap"; type Order = components["schemas"]["Order"]; type OrderStatus = components["schemas"]["OrderStatus"]; type Fill = components["schemas"]["Fill"]; ``` ### Response Fields | Field | Type | Description | | ------------------ | --------------- | ---------------------------------------------------- | | `orderId` | `string` | Unique order identifier | | `version` | `number` | Order version | | `nonce` | `string` | Order nonce | | `sender` | `string` | Sender wallet address | | `recipient` | `string` | Recipient wallet address | | `originChainId` | `number` | Origin chain ID | | `destChainId` | `number` | Destination chain ID | | `tokenIn` | `string` | Input token address | | `tokenOut` | `string` | Output token address | | `amountIn` | `string` | Input amount (smallest unit) | | `amountOut` | `string` | Target output amount (smallest unit) | | `designatedSolver` | `string` | Assigned solver address | | `createdAt` | `number` | Creation timestamp (Unix seconds) | | `fillDeadline` | `number` | Fill deadline timestamp (Unix seconds) | | `status` | `OrderStatus` | Current status (`CREATED`, `COMPLETED`, `CANCELLED`) | | `amountOutFilled` | `string` | Total output filled so far | | `amountInReleased` | `string` | Input amount released so far | | `amountRefunded` | `string` | Amount refunded | | `openTx` | `string | null` | Transaction hash that opened the order | | `fillCount` | `number | null` | Number of fills | | `fillReportCount` | `number | null` | Number of fill reports | | `resolvedAt` | `number | null` | Resolution timestamp when applicable | | `fills` | `Fill[] | null` | Fill events recorded by the indexer, chronological | ### Fill Fields An order may have multiple fills when filled by multiple solvers or in multiple transactions. | Field | Type | Description | | ------------------- | -------- | ---------------------------------- | | `filledAt` | `number` | Fill timestamp (Unix seconds) | | `chainId` | `number` | Chain ID where the fill occurred | | `transactionHash` | `string` | Fill transaction hash | | `solver` | `string` | Solver address that filled | | `amountInToRelease` | `string` | Input amount released by this fill | | `amountOutFilled` | `string` | Output amount filled by this fill | ::note Further inspect the response fields on the [API reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ### Error Responses All errors share the `{ code, message, requestId }` body shape. Branch on `code` rather than on the HTTP status. | Status | Code | Description | | ------ | ----------------- | ------------------------------------------------------------ | | `400` | `BadOrderRequest` | Malformed request (unknown chain, malformed `orderId`, etc.) | | `404` | `OrderNotFound` | The referenced order does not exist on-chain | ## Example Response ```json { "orderId": "0xabc123", "version": 1, "nonce": "42", "sender": "0xYourWalletAddress", "recipient": "0xYourWalletAddress", "originChainId": 1, "destChainId": 8453, "tokenIn": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "tokenOut": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "amountIn": "1000000", "amountOut": "999500", "designatedSolver": "0xSolverAddress", "createdAt": 1771511463, "fillDeadline": 1771515063, "status": "COMPLETED", "amountOutFilled": "999500", "amountInReleased": "1000000", "amountRefunded": "0", "openTx": "0xOpenTxHash", "fillCount": 1, "fillReportCount": 1, "resolvedAt": 1771511780, "fills": [ { "filledAt": 1771511700, "chainId": 8453, "transactionHash": "0xFillTxHash", "solver": "0xSolverAddress", "amountInToRelease": "1000000", "amountOutFilled": "999500" } ] } ``` # GET /orders This endpoint lists orders with optional filters for sender, status, chain IDs, and pagination. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text GET /orders ``` ### Headers ```text x-api-key: YOUR_API_KEY ``` ### Query Parameters | Field | Type | Required | Description | | --------------- | ------------- | -------- | -------------------------------------------------------------- | | `sender` | `string` | No | Filter by sender wallet address | | `status` | `OrderStatus` | No | Filter by order status: `CREATED`, `COMPLETED`, or `CANCELLED` | | `originChainId` | `number` | No | Filter by origin chain ID | | `destChainId` | `number` | No | Filter by destination chain ID | | `limit` | `number` | No | Maximum number of results to return | | `offset` | `number` | No | Pagination offset | ## Example Request ```typescript import type { components } from "./m0-swap"; type OrdersResponse = components["schemas"]["OrdersResponse"]; const params = new URLSearchParams({ sender: "0xYourWalletAddress", status: "CREATED", limit: "20", offset: "0", }); const response = await fetch( `https://gateway.m0.xyz/v1/orchestration/orders?${params.toString()}`, { method: "GET", headers: { "x-api-key": "YOUR_API_KEY", }, }, ); if (!response.ok) { const error = await response.text(); throw new Error(`Get orders failed: ${error}`); } const result: OrdersResponse = await response.json(); ``` ## Response ### Success Response (200) Returns an `OrdersResponse` object. ```typescript import type { components } from "./m0-swap"; type OrdersResponse = components["schemas"]["OrdersResponse"]; type OrderSummary = components["schemas"]["OrderSummary"]; type OrderStatus = components["schemas"]["OrderStatus"]; ``` ### Response Fields | Field | Type | Description | | -------- | ---------------- | -------------------------------- | | `orders` | `OrderSummary[]` | List of matching orders | | `total` | `number` | Total matching records available | | `limit` | `number` | Page size returned | | `offset` | `number` | Pagination offset returned | `OrderSummary` is a compact order shape sourced from the indexer-fed database, so on-chain-only fields (`nonce`, `fillDeadline`, `recipient`, `version`) are not included. Use [`GET /orders/{originChain}/{orderId}`](https://docs.m0.org/api-reference/orchestration/order-status) for the full resource. ::note Further inspect the response fields on the [API reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Example Response ```json { "orders": [ { "orderId": "0xabc123", "createdAt": 1771511463, "originChainId": 1, "sender": "0xYourWalletAddress", "tokenIn": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "amountIn": "1000000", "destChainId": 8453, "tokenOut": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "amountOut": "999500", "designatedSolver": "0xSolverAddress", "openTx": "0xOpenTxHash", "amountOutFilled": "0", "fillCount": 0, "amountInReleased": "0", "fillReportCount": 0, "amountRefunded": "0", "status": "CREATED", "resolvedAt": null } ], "total": 1, "limit": 20, "offset": 0 } ``` ## Notes - Use `limit` and `offset` together for pagination - `status` values are uppercase enum values: `CREATED`, `COMPLETED`, or `CANCELLED` - `createdAt` and `resolvedAt` are Unix timestamps (seconds) # Orchestration API This API provides quotes for moving and converting between M0 extensions and other stablecoins ## Base URL ```text https://gateway.m0.xyz/v1/orchestration ``` ## Authentication The M0 Orchestration API requires authentication via API keys to access its endpoint. This ensures that only authorized users can interact with the API and access its data. [Contact us](https://www.m0.org/contact-us){rel=""nofollow""} to obtain an API key. ### Using API Keys To authenticate your requests, include the API key in the Authentication header of your HTTP requests. For example post with curl: ```sh curl -i \ --request GET \ --header "x-api-key: YOUR_API_KEY" \ https://gateway.m0.xyz/v1/orchestration/supported-assets ``` API keys should be kept secret and not exposed in public repositories or client-side code. If you believe your API key has been compromised, please contact us immediately to revoke the key and issue a new one ## Usage Flow 1. **Check supported assets** — Call [`GET /supported-assets`](https://docs.m0.org/api-reference/orchestration/supported-assets) to discover which tokens and chains are available for routing. 2. **Request a quote** — Call [`POST /quote`](https://docs.m0.org/api-reference/orchestration/quote) with your desired route, amount, and sender address. The response includes ready-to-sign transaction payloads. 3. **Sign and submit** — Sign each payload in order using your wallet and submit the transactions on-chain. Wait for confirmation before sending the next payload. 4. **Track your order** — For routes through the [`limit-order` provider](https://docs.m0.org/protocol/limit-order-protocol), monitor progress on the [M0 Explorer](https://explorer.m0.org){rel=""nofollow""}, or use the [`GET /orders/{originChain}/{orderId}`](https://docs.m0.org/api-reference/orchestration/order-status) endpoint to check order status programmatically. The `orderId` is returned as part of the limit-order quote/execution flow. You can also use [`GET /orders`](https://docs.m0.org/api-reference/orchestration/orders) to list existing orders and find the relevant `orderId`. For all other providers, a transaction hash is returned directly when submitting the payload on-chain. ## Endpoints - `GET /supported-assets` - `POST /quote` - `POST /permit/quote` - `POST /permit/build` - `GET /orders` (For transactions using the `limit-order` provider) - `GET /orders/{originChain}/{orderId}` - `POST /orders/{originChain}/{orderId}/cancel` For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. ## Errors All API error responses share the same body shape: ```json { "code": "BadQuoteRequest", "message": "Human-readable description of what went wrong", "requestId": "abc-123" } ``` - `code` — stable machine-readable identifier for the failure mode. Branch on it rather than on the HTTP status, since multiple errors may map to the same status over time. - `message` — human-readable description, safe to surface in a UI. - `requestId` — per-request correlation ID matching the server logs. Include it when filing support tickets. # Permit Approvals Permit approvals let you collapse the usual two-transaction flow — an on-chain `approve` followed by the action — into a single transaction backed by an off-chain signature. Instead of approving a spender on-chain, the user signs an [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612){rel=""nofollow""} permit (EIP-712 typed data) and the action contract pulls the allowance from that signature when it runs. The flow is two steps: 1. [`POST /permit/quote`](https://docs.m0.org/#post-permitquote) returns the quote economics plus a permit *recipe* — the EIP-712 typed data to sign — for each leg of the route. 2. The client signs each recipe's `typedData`, then either calls [`POST /permit/build`](https://docs.m0.org/#post-permitbuild) with the signed recipes to get executable payloads, or encodes the call locally (see [Self-encoding](https://docs.m0.org/#self-encoding)). ::note **EVM-only and opt-in.** Permit approvals are an EVM-only feature you opt into. EIP-2612 requires the input token to implement `permit()` — all M and wM extensions and USDC do. Routes whose input token does not support `permit()` return `404 PermitNotSupported`. :: ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## POST /permit/quote Returns one or more quotes for a given asset route and amount, each carrying a permit recipe per leg. A recipe contains the EIP-712 typed data the client signs. ### Request #### Endpoint ```text POST /permit/quote ``` #### Headers ```text Content-Type: application/json x-api-key: YOUR_API_KEY ``` #### Body Parameters and response Check the [API reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""} for detailed schema definitions. ### Example Request ```typescript import type { components } from "./m0-swap"; // From type generation type PermitQuoteResponse = components["schemas"]["PermitQuoteResponse"]; const response = await fetch( "https://gateway.m0.xyz/v1/orchestration/permit/quote", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, body: JSON.stringify({ route: { source: { chain: "Ethereum", address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC token }, destination: { chain: "Solana", address: "xoUSDq85Rjsb6SbUwJyreFgeWQvxdkT7R3c3g7s6p5Y", // XO token }, }, amountIn: "1000000", // 1 USDC (6 decimals) funder: "0xYourWalletAddress", sender: "0xYourWalletAddress", recipient: "yourSolanaWalletAddress", }), }, ); const result: PermitQuoteResponse = await response.json(); // Getting first quote and first recipe for signing const quote = result.quotes[0]; const recipe = quote.recipes[0]; // See Self-encoding section for how to sign the recipe and encode the call locally. ``` ### Error Responses All errors share the `{ code, message, requestId }` body shape. Branch on `code` rather than on the HTTP status. | Status | Code | Description | | ------ | -------------------- | ------------------------------------------------------------------------- | | `400` | `BadPermitRequest` | Malformed or invalid body (missing fields, unsupported asset, bad params) | | `404` | `PermitNotSupported` | The route is valid but not permit-capable (input token has no `permit()`) | | `500` | `PermitQuoteError` | Unexpected internal failure while computing the permit quote | ## POST /permit/build Takes the signed recipes and returns executable transaction payloads — the same `Payload` shape returned by [`POST /quote`](https://docs.m0.org/api-reference/orchestration/quote). Use this if you want the server to encode the `*WithPermit` call for you; otherwise see [Self-encoding](https://docs.m0.org/#self-encoding). ### Request #### Endpoint ```text POST /permit/build ``` #### Headers ```text Content-Type: application/json x-api-key: YOUR_API_KEY ``` #### Body Parameters `PermitBuildParams`: | Field | Type | Required | Description | | --------------- | ---------------- | -------- | ------------------------------- | | `signedRecipes` | `SignedRecipe[]` | Yes | The signed recipes, one per leg | Each `SignedRecipe`: | Field | Type | Required | Description | | ----------- | -------------- | -------- | -------------------------------------------------------- | | `recipe` | `PermitRecipe` | Yes | The recipe echoed verbatim from `/permit/quote` | | `signature` | `string` | Yes | The 65-byte hex permit signature over `recipe.typedData` | ### Response `PermitBuildResponse`: | Field | Type | Description | | ----------- | -------------- | -------------------------------------------------- | | `requestId` | `string` | Identifier for the request, echoed in error bodies | | `payloads` | `Payload[]` | Executable payloads (same shape as `/quote`) | | `issues` | `PermitIssues` | Optional soft warnings; does not fail the request | Each `Payload` is `{ provider, annotation?, data: { type: "evm", chain, chainId, to, data, value } }`, identical to the payloads returned by [`POST /quote`](https://docs.m0.org/api-reference/orchestration/quote). Execute them in order — see [Executing Quote Payloads](https://docs.m0.org/api-reference/orchestration/quote#executing-quote-payloads). ### Example Request ```typescript import type { components } from "./m0-swap"; // From type generation type PermitBuildResponse = components["schemas"]["PermitBuildResponse"]; const response = await fetch( "https://gateway.m0.xyz/v1/orchestration/permit/build", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, body: JSON.stringify({ signedRecipes: [ { recipe: quote.recipes[0], // echoed verbatim from /permit/quote signature: "0x...", // 65-byte signature over recipe.typedData }, ], }), }, ); const result: PermitBuildResponse = await response.json(); ``` ### Example Response ```json { "requestId": "req_abc123", "payloads": [ { "provider": "limit-order", "annotation": "Limit Order: USDC → XO", "data": { "type": "evm", "chain": "Ethereum", "chainId": 1, "to": "0xSwapFacilityAddress", "data": "0xabcdef...", "value": "0" } } ] } ``` ### Error Responses All errors share the `{ code, message, requestId }` body shape. | Status | Code | Description | | ------ | ------------------------ | --------------------------------------------------------------------------- | | `400` | `BadPermitRequest` | Malformed or invalid body | | `410` | `PermitExpired` | The recipe's permit deadline has passed; re-quote to get a fresh recipe | | `422` | `InvalidPermitSignature` | `ecrecover` does not match `owner`, or the signature is over a wrong domain | | `500` | `PermitBuildError` | Unexpected internal failure while building the payloads | ## Self-encoding You do not have to send the signature back to the server. The recipe carries everything needed to build the final transaction calldata locally — useful if you would rather not round-trip the signature. The flow is: 1. Sign `recipe.typedData` to get the 65-byte permit signature. 2. Encode the call named by `recipe.functionSignature`, using `recipe.operation` args plus the signature. 3. Send the transaction to `recipe.spender` with `recipe.value`. The example below signs the typed data with [Viem](https://viem.sh){rel=""nofollow""} and encodes an M Swap Facility `swapWithPermit(...)` call — the `bytes signature` overload: ```typescript import { encodeFunctionData, parseAbi, type WalletClient } from "viem"; import type { components } from "./m0-swap"; // From type generation type PermitRecipe = components["schemas"]["PermitRecipe"]; // The standard EIP-2612 Permit type set (omitted from the recipe payload). const eip2612Types = { Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], } as const; async function encodeSwapWithPermit( recipe: PermitRecipe, wallet: WalletClient, ) { // 1. Sign the recipe's typed data -> 65-byte permit signature. const signature = await wallet.signTypedData({ account: wallet.account!, domain: recipe.typedData.domain, types: eip2612Types, primaryType: "Permit", message: recipe.typedData.message, }); // 2. Encode the call straight from `recipe.functionSignature` — no need to // hardcode the ABI. For the M Swap Facility this is the `bytes` overload. const op = recipe.operation; // type: "swap" const functionName = recipe.functionSignature.split("(")[0]; const abi = parseAbi([`function ${recipe.functionSignature}`]); const data = encodeFunctionData({ abi, functionName, args: [ op.extensionIn, op.extensionOut, BigInt(op.amount), op.recipient, BigInt(recipe.typedData.message.deadline), signature, ], }); // 3. The transaction: send to recipe.spender with recipe.value. return { to: recipe.spender as `0x${string}`, data, value: BigInt(recipe.value), }; } ``` ::note Uniswap's `ammSwap` operation does not take a packed `bytes` signature — its `selfPermit` expects the signature split into `v`, `r`, and `s`. Slice the 65-byte signature with Viem (`r = slice(sig, 0, 32)`, `s = slice(sig, 32, 64)`, `v = hexToNumber(slice(sig, 64, 65))`) before encoding the call. :: ## Notes - Permit approvals are EVM-only and opt-in — use the standard [`POST /quote`](https://docs.m0.org/api-reference/orchestration/quote) flow when you do not need a single-transaction approval. - A quote is no longer buildable after `expiresAt` (the earliest permit deadline across its recipes). Re-quote to get fresh recipes. - `minAmountOut` is the floor that must be met or the transaction reverts; it equals `amountOut` for exact-output providers. AMM legs (e.g. Uniswap) apply the server's standard slippage tolerance. - The build step encodes the same `Payload` shape as `/quote`, so payload execution is identical. - Verify the recipe's `typedData` before signing, and the built payloads before executing — see [Verifying Payloads](https://docs.m0.org/api-reference/orchestration/verify-payloads#permits). # POST /quote This endpoint returns one or more quotes for a given asset route and amount. Each quote includes the expected output amount, estimated fill time, and transaction payloads ready to be signed and submitted to the blockchain. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text POST /quote ``` ### Headers ```text Content-Type: application/json x-api-key: YOUR_API_KEY ``` ### Body Parameters | Field | Type | Required | Description | | -------------- | ----------------- | -------- | -------------------------------------------------------------------------------- | | `route` | `Route` | Yes | The `source` and `destination` assets, each a `{ chain, address }` pair | | `amountIn` | `string` | Yes | Input amount in the token's smallest unit | | `sender` | `string` | Yes | The wallet address sending the input asset | | `recipient` | `string` | No | The address receiving the output asset (defaults to `sender`) | | `maxNumQuotes` | `number` | No | Maximum number of quotes to return (default `1`) | | `providers` | `ProvidersParams` | No | `include` or `exclude` lists of providers to constrain routing (set at most one) | Check the [API reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""} for detailed schema definitions. ## Example Request ```typescript import type { components } from "./m0-swap"; // From type generation type Quote = components["schemas"]["Quote"]; const response = await fetch("https://gateway.m0.xyz/v1/orchestration/quote", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, body: JSON.stringify({ route: { source: { chain: "Ethereum", address: "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", // M token }, destination: { chain: "Base", address: "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", // M token }, }, amountIn: "1000000", // 1 M (6 decimals) sender: "0xYourWalletAddress", maxNumQuotes: 1, }), }); const quotes: Quote[] = await response.json(); ``` ## Example Response ```json [ { "route": { "source": { "chain": "Ethereum", "address": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b" }, "destination": { "chain": "Base", "address": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b" } }, "recipient": "0xYourWalletAddress", "amountIn": "1000000", "amountOut": "999500", "estFillTime": 180, "payloads": [ { "provider": "m-wormhole-portal", "annotation": "Bridge M from Ethereum to Base", "data": { "type": "evm", "chain": "Ethereum", "chainId": 1, "to": "0x1234567890abcdef...", "data": "0xabcdef...", "value": "0" } } ] } ] ``` ## Error Responses All errors share the `{ code, message, requestId }` body shape. Branch on `code` rather than on the HTTP status. | Status | Code | Description | | ------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `400` | `BadQuoteRequest` | Malformed or invalid body (missing fields, same source and destination, both `include` and `exclude` set, unsupported asset) | | `404` | `NoQuotesAvailable` | The request was valid but no provider could produce a quote for this route and amount | | `500` | `QuoteError` | Unexpected internal failure while computing quotes — retrying may succeed | ## Executing Quote Payloads Once you have a quote, you can execute its payloads using your preferred chain interface. ::warning{title="Verify before signing"} Payloads are fully-encoded transactions. Before signing, check that the amounts, recipient, and target contracts match your request — see [Verifying Payloads](https://docs.m0.org/api-reference/orchestration/verify-payloads). :: Here is an example executing a payload on Base using [Viem](https://viem.sh){rel=""nofollow""}. ### EVM Payload Execution ```typescript import type { components } from "./m0-swap"; // From type generation import { createPublicClient, http } from "viem"; import { base } from "viem/chains"; type EvmPayload = components["schemas"]["EvmPayload"]; async function executeEvmPayload( payload: EvmPayload, account: ReturnType, ) { const client = createPublicClient({ chain: base, transport: http(), }); // Send the transaction const hash = await client.sendTransaction({ to: payload.to as `0x${string}`, data: payload.data as `0x${string}`, value: BigInt(payload.value), }); // Wait for confirmation const receipt = await client.waitForTransactionReceipt({ hash, confirmations: 1, }); return { hash, receipt }; } ``` ## Filtering Providers Use the optional `providers` field to control which liquidity providers are used when generating quotes. You can either include only specific providers or exclude certain ones. Check the [API reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""} for all supported providers and their identifiers. ### Include Only Specific Providers ```typescript const body = JSON.stringify({ route: { /* ... */ }, amountIn: "1000000", sender: "0xYourWalletAddress", providers: { include: ["m-wormhole-portal", "m-swap-facility"], }, }); ``` ### Exclude Specific Providers ```typescript const body = JSON.stringify({ route: { /* ... */ }, amountIn: "1000000", sender: "0xYourWalletAddress", providers: { exclude: ["wormhole-cctp"], }, }); ``` ::note If both `include` and `exclude` are provided, `include` takes precedence — only the listed providers will be used, and `exclude` is ignored. :: ## Notes - The `amountIn` parameter should be provided in the token's smallest unit (e.g., for a 6-decimal token, `1000000` represents 1 token) - Results are ranked by best `amountOut`, fastest `estFillTime`, and fewest payloads (in that order), and the response is capped at `maxNumQuotes` (default `1`) - Payloads should be executed in order - wait for each transaction to confirm before sending the next - Cross-chain quotes may include multiple payloads (e.g., approve + bridge) - The `estFillTime` is an estimate in seconds for the entire route to complete # GET /supported-assets This endpoint returns a list of all tokens that can be used as source or destination assets when requesting quotes. Each asset includes chain information, contract address, symbol, decimals, and whether it's an M0 protocol extension. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text GET /supported-assets ``` ### Headers ```text x-api-key: YOUR_API_KEY ``` ### Parameters This endpoint accepts no parameters. ## Response ### Success Response (200) Returns an array of `Asset` objects. ```typescript // Using generated types (see type generation) import type { components } from "./m0-swap"; type Asset = components["schemas"]["Asset"]; type Chain = components["schemas"]["Chain"]; type ChainRuntime = components["schemas"]["ChainRuntime"]; ``` ### Asset Fields | Field | Type | Description | | ------------- | -------------- | ------------------------------------------------ | | `chain` | `Chain` | The blockchain network where this asset exists | | `runtime` | `ChainRuntime` | The chain's runtime environment (`evm` or `svm`) | | `address` | `string` | Token contract address | | `icon` | `string` | URL to the token's icon image | | `decimals` | `number` | Number of decimal places for the token | | `symbol` | `string` | Token ticker symbol (e.g., "M", "USDC") | | `name` | `string` | Full token name | | `m0Extension` | `boolean` | Whether this token is an M protocol extension | ## Example Request ```typescript import type { components } from "./m0-swap"; type Asset = components["schemas"]["Asset"]; const response = await fetch( "https://gateway.m0.xyz/v1/orchestration/supported-assets", { headers: { "x-api-key": "YOUR_API_KEY", }, }, ); const assets: Asset[] = await response.json(); ``` ## Example Response ```json [ { "chain": "Ethereum", "runtime": "evm", "address": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "icon": "https://example.com/m-token.png", "decimals": 6, "symbol": "M", "name": "M Token", "m0Extension": true }, { "chain": "Ethereum", "runtime": "evm", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "icon": "https://example.com/usdc.png", "decimals": 6, "symbol": "USDC", "name": "USD Coin", "m0Extension": false }, { "chain": "Solana", "runtime": "svm", "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "icon": "https://example.com/usdc.png", "decimals": 6, "symbol": "USDC", "name": "USD Coin", "m0Extension": false }, { "chain": "Base", "runtime": "evm", "address": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "icon": "https://example.com/m-token.png", "decimals": 6, "symbol": "M", "name": "M Token", "m0Extension": true } ] ``` ## Notes - Assets may appear on multiple chains with the same symbol but different addresses - The `runtime` field helps determine which wallet type is needed (EVM wallet vs Solana wallet) - Use the `decimals` field to properly format amounts when displaying to users or constructing quote requests - The `m0Extension` field identifies tokens that are part of the M protocol ecosystem # TypeScript Type Generation ## Quick Start ```bash npx openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./m0-swap.d.ts ``` This generates a type definition file with full type coverage for all API endpoints, request parameters, and response objects. ## Generated Types The generated file exports types organized by OpenAPI structure: ```typescript import type { components, operations } from "./m0-swap"; // Schema types (most commonly used) type Asset = components["schemas"]["Asset"]; type Quote = components["schemas"]["Quote"]; type Order = components["schemas"]["Order"]; type OrdersResponse = components["schemas"]["OrdersResponse"]; type OrderStatus = components["schemas"]["OrderStatus"]; type Route = components["schemas"]["Route"]; type Chain = components["schemas"]["Chain"]; type Payload = components["schemas"]["Payload"]; type EvmPayload = components["schemas"]["EvmPayload"]; type SvmPayload = components["schemas"]["SvmPayload"]; // Operation types (for request/response typing) type QuoteRequest = operations["quote_quote"]["requestBody"]["content"]["application/json"]; type QuoteResponse = operations["quote_quote"]["responses"]["200"]["content"]["application/json"]; ``` ## Usage with fetch Create a typed wrapper for API calls: ```typescript import type { components } from "./m0-swap"; type Quote = components["schemas"]["Quote"]; type Asset = components["schemas"]["Asset"]; type Order = components["schemas"]["Order"]; type OrdersResponse = components["schemas"]["OrdersResponse"]; type Chain = components["schemas"]["Chain"]; const API_BASE = "https://gateway.m0.xyz/v1/orchestration"; export async function getQuotes(params: { route: { source: { chain: string; address: string }; destination: { chain: string; address: string }; }; amountIn: string; sender: string; recipient?: string; maxNumQuotes?: number; }): Promise { const response = await fetch(`${API_BASE}/quote`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, body: JSON.stringify(params), }); return response.json(); } export async function getSupportedAssets(): Promise { const response = await fetch(`${API_BASE}/supported-assets`, { headers: { "x-api-key": YOUR_API_KEY }, }); return response.json(); } export async function getOrders(params?: { sender?: string; status?: "CREATED" | "COMPLETED" | "CANCELLED"; originChainId?: number; destChainId?: number; limit?: number; offset?: number; }): Promise { const query = new URLSearchParams(); if (params?.sender) query.set("sender", params.sender); if (params?.status) query.set("status", params.status); if (params?.originChainId !== undefined) query.set("originChainId", String(params.originChainId)); if (params?.destChainId !== undefined) query.set("destChainId", String(params.destChainId)); if (params?.limit !== undefined) query.set("limit", String(params.limit)); if (params?.offset !== undefined) query.set("offset", String(params.offset)); const queryString = query.toString(); const url = queryString ? `${API_BASE}/orders?${queryString}` : `${API_BASE}/orders`; const response = await fetch(url, { headers: { "x-api-key": YOUR_API_KEY }, }); return response.json(); } export async function getOrder( originChain: Chain, orderId: string, ): Promise { const response = await fetch(`${API_BASE}/orders/${originChain}/${orderId}`, { headers: { "x-api-key": YOUR_API_KEY }, }); return response.json(); } ``` ## Type Aliases For convenience, create a types file that re-exports commonly used types: ```typescript // types/m0-swap.ts import type { components } from "./m0-swap.d.ts"; export type Asset = components["schemas"]["Asset"]; export type Quote = components["schemas"]["Quote"]; export type Order = components["schemas"]["Order"]; export type OrderSummary = components["schemas"]["OrderSummary"]; export type OrdersResponse = components["schemas"]["OrdersResponse"]; export type OrderStatus = components["schemas"]["OrderStatus"]; export type Fill = components["schemas"]["Fill"]; export type Route = components["schemas"]["Route"]; export type AssetAddress = components["schemas"]["AssetAddress"]; export type Payload = components["schemas"]["Payload"]; export type TransactionPayload = components["schemas"]["TransactionPayload"]; export type EvmPayload = components["schemas"]["EvmPayload"]; export type SvmPayload = components["schemas"]["SvmPayload"]; export type Chain = components["schemas"]["Chain"]; export type ChainRuntime = components["schemas"]["ChainRuntime"]; export type Providers = components["schemas"]["Providers"]; export type ProvidersParams = components["schemas"]["ProvidersParams"]; ``` Then import from your alias file: ```typescript import type { Quote, Asset, EvmPayload } from "./types/m0-swap"; ``` ## Keeping Types Updated Regenerate types when the API changes: ```bash # Add to package.json scripts { "scripts": { "generate:types": "openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./src/types/m0-swap.d.ts" } } ``` ```bash pnpm generate:types ``` ## openapi-typescript Options Common options for customization: ```bash # Output to specific directory npx openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./src/types/api.d.ts # Generate with immutable types (readonly) npx openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./api.d.ts --immutable # Export types (for re-exporting) npx openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./api.d.ts --export-type ``` See the [openapi-typescript documentation](https://openapi-ts.dev/){rel=""nofollow""} for more options. ## API Reference For interactive schema exploration and testing, visit the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. # Verify Payloads [`POST /quote`](https://docs.m0.org/api-reference/orchestration/quote) returns fully-encoded transaction payloads, ready to sign and submit. This page explains what to check in those payloads before signing, so you can confirm the quote is valid and the transaction is safe. ::note [**The Deployments pages**](https://docs.m0.org/resources/addresses) list every M0 contract and program per chain, with explorer and ABI links. Pin your allowlist to those pages and not to addresses observed in previous API responses. :: ## Quote-level checks Every `Quote` echoes your request back. Before looking at individual payloads, confirm: | Field | Check | | ----------- | ------------------------------------------------------------------------------ | | `route` | `source` and `destination` match the chain and token address you requested | | `recipient` | Matches the `recipient` you passed, or your `sender` if you omitted it | | `amountIn` | Matches the amount you requested, in the token's smallest unit | | `amountOut` | Within your expectations for the route; reject quotes with an implausible rate | Then verify each entry in `payloads` according to its runtime, discriminated by `payload.data.type` (`"evm"` or `"svm"`). Payloads execute in order, so check all of them before signing the first. ## EVM payloads An EVM payload's `data` is a complete transaction for your wallet to sign. Its shape is `{ type: "evm", chain, chainId, to, data, value }`. Check: - **Chain**. `chain` and `chainId`. Pin that mapping yourself rather than deriving it from the response. The first leg always executes on `route.source.chain`, and approvals execute on the same chain as the leg they approve. Multi-hop routes include legs on intermediate chains: each leg must execute either on the same chain as the previous leg or on the destination chain decoded from the previous bridge leg's calldata. - **Target contract (`to`) is known**. For action legs (swap, bridge, order), `to` must be a contract listed on the [Deployments](https://docs.m0.org/resources/addresses) pages (e.g. `SwapFacility`, a Portal). For approval legs, `to` is the input token itself, the address from your `route.source`, which also appears on the Deployments pages and in [`GET /supported-assets`](https://docs.m0.org/api-reference/orchestration/supported-assets). For third-party legs (`wormhole-cctp`), verify against that provider's official deployment docs. - **Amount**. Decode the calldata and confirm the amount equals the quote's `amountIn`. - **Recipient**. On the final leg, confirm the decoded recipient equals the quote's `recipient`. - **Approvals are exact and correctly scoped**. An approval leg is `approve(spender, amount)` where `spender` is the contract executing the next payload and `amount` is exactly `amountIn`. The API never asks for unlimited approvals; reject any payload that does. - **Native value**. `value` is `"0"` for approvals and same-chain swaps. Bridge legs carry the cross-chain delivery fee in `value`; sanity-check its magnitude against current gas prices. ### Function signatures by provider Each payload's `provider` field determines the exact function to expect in the calldata. After decoding, confirm the function matches this list and reject the payload if it calls anything else: - **M0 Limit Orders, `limit-order`** - [0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4](https://etherscan.io/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4){rel=""nofollow""}:br Source: [OrderBook.openOrder()](https://github.com/m0-platform/liquidity-delivery/blob/77a86d059d30fd5af7da97507af2e48f2c7d68ee/evm/src/OrderBook.sol#L89){rel=""nofollow""} ```solidity openOrder(uint32 destChainId, uint32 fillDeadline, address tokenIn, bytes32 tokenOut, uint128 amountIn, uint128 amountOut, bytes32 recipient, bytes32 solver, address sender) ``` - **M0 Swap Facility, `m-swap-facility`** - [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://etherscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""}:br Source: [SwapFacility.swap()](https://github.com/m0-platform/evm-m-extensions/blob/50bc58a5a14a5778c0f1363ff4b2f6bc77cfdc66/src/swap/SwapFacility.sol#L104){rel=""nofollow""}. Used for both extension ↔ extension and USDC → extension swaps. ```solidity swap(address extensionIn, address extensionOut, uint256 amount, address recipient) ``` - **M0 Portal, `m-portal`** - [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://etherscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""}:br Source: [Portal.sendToken()](https://github.com/m0-platform/m-portal-v2/blob/e33f24827afd5ed12ce389dac6f3de7ec3faa9dc/evm/src/Portal.sol#L127){rel=""nofollow""}. Features [Hyperlane `0xfCc...0f32`](https://etherscan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} and [LayerZero `0x77E...4b4d`](https://etherscan.io/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} bridge adapters. ```solidity sendToken(uint256 amount, address sourceToken, uint32 destinationChainId, bytes32 destinationToken, bytes32 recipient, bytes32 refundAddress, bytes bridgeAdapterArgs) ``` - **M0 Wormhole Portal, `m-wormhole-portal`** - [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://etherscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""}:br Same Portal V2 contract featuring [Wormhole `0xaCf...4fDd`](https://etherscan.io/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd){rel=""nofollow""} bridge adapter as the designated bridge. See [Bridging for Developers](https://docs.m0.org/build/bridging-developer) for more details. ```solidity sendToken(uint256 amount, address sourceToken, uint32 destinationChainId, bytes32 destinationToken, bytes32 recipient, bytes32 refundAddress, address bridgeAdapter, bytes bridgeAdapterArgs) ``` - **PYUSDx Swap Facility, `pyusdx-swap-facility`** - [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://etherscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""}:br Source: [SwapFacility.swap()](https://etherscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code){rel=""nofollow""}. Same design as the M0 Swap Facility above. ```solidity swap(address tokenIn, address tokenOut, uint256 amount, address recipient) ``` - **PYUSDx Portal, `pyusdx-portal`** - [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://etherscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""}:br Source: [Portal.sendToken()](https://etherscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code){rel=""nofollow""} ```solidity sendToken(uint256 amount, address sourceToken, uint32 destinationChainId, bytes32 destinationToken, bytes32 recipient, bytes32 refundAddress) ``` - **Wormhole CCTP, `wormhole-cctp`**. Third-party USDC bridging through Circle CCTP v2 and the Wormhole executor. The API does not encode these payloads itself; verify the target contracts and calldata against [Wormhole's](https://wormhole.com/docs/){rel=""nofollow""} and [Circle's](https://developers.circle.com/stablecoins/cctp-getting-started){rel=""nofollow""} official docs. - **Approval legs** (any provider) — a standard ERC-20 approval on the input token: ```solidity approve(address spender, uint256 amount) ``` ### Example transactions Known-good mainnet transactions to compare your decoded calldata against. The `fillOrder()` and inbound delivery rows are the counterpart transactions executed by solvers and bridge relayers, you never sign those, but they show what completing each flow looks like on the destination chain: | Provider | Function | Transaction | | ---------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `limit-order` | `openOrder()`, same-chain | [Ethereum](https://etherscan.io/tx/0xba1c55380b402cb8b4d1c73c2147ff48eb2620966cc6941d5e2608e0a7cafcf5){rel=""nofollow""} | | `limit-order` | `fillOrder()`, same-chain fill | [Ethereum](https://etherscan.io/tx/0x1a5ab6ddccc123b78dc0441ddc2aec84d93c808dc6a4bac159c76d086a1edf07){rel=""nofollow""} | | `limit-order` | `openOrder()`, cross-chain | [Ethereum](https://etherscan.io/tx/0x2555baa66e0355426b08d676d6d14e44dfee6af1b17c24c7f647fbb357e8ebcb){rel=""nofollow""} | | `limit-order` | `fillOrder()`, cross-chain fill | [Citrea](https://citreascan.com/tx/0xab7b9c01b6ab4c8297efdd3dfc39f374c44310c5d6b65e164b3cb9ee339b2ff4){rel=""nofollow""} | | `m-swap-facility` | `swap()`, USDC → extension | [Ethereum](https://etherscan.io/tx/0x59949353cbdcae4833241bc11e890032b2ba3924096bc963513c4bf7e7ed3ce8){rel=""nofollow""} | | `m-portal` | `sendToken()`, outbound | [Ethereum](https://etherscan.io/tx/0xe5ebdd85c657577472e4716f4d10df2032df70895770cf8e136a60ae652e1ee5){rel=""nofollow""} | | `m-portal` | inbound delivery | [Monad](https://monadscan.com/tx/0xb2e66666814d9365ac65d83df848cc266225c936312ac8df8b4b7159ea27b84a){rel=""nofollow""} | | `pyusdx-swap-facility` | `swap()` | [Ethereum](https://etherscan.io/tx/0x5a6e96708faef3049f083e0b54784110fe2ad12abd328cab42a4779d4d08d734){rel=""nofollow""} | | `pyusdx-portal` | `sendToken()` | [Ethereum](https://etherscan.io/tx/0x9cb82042188d3ee6e94f94f3f6c6ea421bb0278e2e965eb257ab4b1da9faa8d3){rel=""nofollow""} | ### Example: decoding and checking an approval leg ```typescript import { decodeFunctionData, erc20Abi } from "viem"; import type { components } from "./m0-swap"; // From type generation type Quote = components["schemas"]["Quote"]; type EvmPayload = components["schemas"]["EvmPayload"]; function verifyApprovalLeg( quote: Quote, payload: EvmPayload, nextLegTo: string, ) { // Approval legs call approve() on the input token itself if (payload.to.toLowerCase() !== quote.route.source.address.toLowerCase()) { throw new Error("approval target is not the input token"); } const { functionName, args } = decodeFunctionData({ abi: erc20Abi, data: payload.data as `0x${string}`, }); if (functionName !== "approve") throw new Error("unexpected function"); const [spender, amount] = args; if (spender.toLowerCase() !== nextLegTo.toLowerCase()) { throw new Error("spender is not the contract executing the next leg"); } if (amount !== BigInt(quote.amountIn)) { throw new Error("approval amount does not equal amountIn"); } } ``` For action legs, decode against the contract's ABI (linked from the [Deployments](https://docs.m0.org/resources/addresses) tables) and apply the same amount and recipient checks. As a final safety net, simulate the transaction (`eth_call` / Viem `simulateContract`) and inspect the resulting balance changes before broadcasting. ### Permits The opt-in [permit flow](https://docs.m0.org/api-reference/orchestration/permit-approvals) replaces the on-chain approval with an off-chain [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612){rel=""nofollow""} signature, so the verification target shifts: what you sign is the recipe's `typedData`, and a bad permit is as dangerous as a bad approval. Before signing a recipe from `POST /permit/quote`, check: | Field | Check | | ------------------------------------ | --------------------------------------------------------------------------------------------------------- | | `typedData.message.owner` | Your address (the account funding the input tokens) | | `typedData.message.spender` | Equals `recipe.spender` and is a known contract on [Deployments](https://docs.m0.org/resources/addresses) | | `typedData.message.value` | Exactly the quote's `amountIn`, never unlimited | | `typedData.domain.verifyingContract` | The input token address from your `route.source` | | `typedData.domain.chainId` | The source chain's id | | `typedData.message.deadline` | Near-future timestamp, not far beyond the quote's `expiresAt` | Payloads returned by [`POST /permit/build`](https://docs.m0.org/api-reference/orchestration/permit-approvals#post-permitbuild) are the same `EvmPayload` shape as `/quote`, so all the EVM checks above apply to them unchanged. The one difference is the function in the calldata: instead of the plain entrypoints listed in [Function signatures by provider](https://docs.m0.org/#function-signatures-by-provider), a built payload calls the provider's permit variant, which appends the permit `deadline` and `signature`: - **Swap Facilities `m-swap-facility`** and **`pyusdx-swap-facility`**: ```solidity swapWithPermit(address extensionIn, address extensionOut, uint256 amount, address recipient, uint256 deadline, bytes signature) ``` - **M0 Limit orders `limit-order`**: ```solidity openOrderWithPermit((uint32 destChainId, uint32 fillDeadline, address tokenIn, bytes32 tokenOut, uint128 amountIn, uint128 amountOut, bytes32 recipient, bytes32 solver, address sender) orderParams, uint256 deadline, bytes permitSignature) ``` The recipe echoes the exact canonical signature in its `functionSignature` field. The decoded calldata must match it. You can also skip the build round-trip entirely: the recipe carries everything needed to encode the final calldata offline, which means you construct, and therefore fully control, the exact transaction you sign. See [Self-encoding](https://docs.m0.org/api-reference/orchestration/permit-approvals#self-encoding). ## SVM payloads An SVM payload is `{ type: "svm", chain, transaction }`, where `transaction` is a base64-encoded Solana `VersionedTransaction`. Multi-step Solana routes are combined server-side into a single transaction where possible, and the server pre-signs any auxiliary signers it controls. Your wallet adds the fee-payer signature. Deserialize to inspect the transaction. Check: - **Fee payer is your sender**. The first account key (the fee payer and first required signer) must be the `sender` you passed to `/quote`. - **Every invoked program is known**. Each instruction's program id must be one you recognize: the M0 programs listed in the Solana section of [Deployments](https://docs.m0.org/resources/addresses), standard Solana programs (System, Compute Budget, SPL Token / Token-2022, Associated Token Account), or the bridge programs of the leg's provider (e.g. Wormhole). - **Resolve lookup tables first**. Versioned transactions may load accounts through address lookup tables, so fetch the tables and audit the *resolved* key set, not just the static keys. - **Amount and recipient**. The destination token account must be the associated token account of the quote's `recipient` for the destination mint, and the transferred amount must equal `amountIn`. The most robust way to check both is simulation: run `simulateTransaction` and inspect the resulting token balance changes. - **Keep existing signatures intact**. The server may have already signed with auxiliary signers. Add your signature to the transaction as-is; mutating the message invalidates the pre-applied signatures. ### Instructions by provider As on EVM, the payload's `provider` field determines which program instructions to expect (alongside Compute Budget and, when the destination token account is missing, an Associated Token Account `create` instruction): - **M0 Swap Facility `m-swap-facility`**, the `swap(amount, remainingAccountsCount)` instruction on the M0 Swap Facility program. The signer account must be your sender and the destination token account must belong to the quote's `recipient`. - **M0 Wormhole Portal `m-wormhole-portal`**, the `sendToken(amount, destinationToken, destinationChainId, recipient)` instruction on the M0 Portal program, followed by a relay-request instruction on the Wormhole Executor program that pays the delivery fee to the executor's payee account. - **M0 Limit Orders `limit-order`**, the `openOrder` instruction on the M0 OrderBook program, carrying the same order params as the EVM variant (`destChainId`, `fillDeadline`, `tokenIn`, `tokenOut`, `amountIn`, `amountOut`, `recipient`, `solver`). - **Wormhole CCTP `wormhole-cctp`**, third-party: Circle CCTP v2 and Wormhole executor programs; verify against those providers' official docs. ### Example: inspecting an SVM payload ```typescript import { Connection, PublicKey, VersionedTransaction } from "@solana/web3.js"; import type { components } from "./m0-swap"; // From type generation type SvmPayload = components["schemas"]["SvmPayload"]; // Pin to /resources/addresses. Do not learn addresses from API responses const KNOWN_PROGRAMS = new Set([ "mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY", // M0 Portal "11111111111111111111111111111111", // System "ComputeBudget111111111111111111111111111111", // Compute Budget "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", // Token-2022 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // SPL Token "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", // Associated Token Account // ...plus the provider's bridge programs (e.g. Wormhole) for cross-chain legs ]); async function verifySvmPayload( connection: Connection, payload: SvmPayload, sender: string, ) { const tx = VersionedTransaction.deserialize( Buffer.from(payload.transaction, "base64"), ); // 1. Fee payer (first required signer) must be your sender const feePayer = tx.message.staticAccountKeys[0]; if (!feePayer.equals(new PublicKey(sender))) { throw new Error("unexpected fee payer"); } // 2. Resolve address lookup tables so every account key is visible const lookupTables = await Promise.all( tx.message.addressTableLookups.map( async (lookup) => (await connection.getAddressLookupTable(lookup.accountKey)).value!, ), ); const accounts = tx.message.getAccountKeys({ addressLookupTableAccounts: lookupTables, }); // 3. Every invoked program must be one you recognize for (const ix of tx.message.compiledInstructions) { const programId = accounts.get(ix.programIdIndex)!.toBase58(); if (!KNOWN_PROGRAMS.has(programId)) { throw new Error(`unknown program: ${programId}`); } } // 4. Simulate and inspect balance changes before signing const sim = await connection.simulateTransaction(tx, { sigVerify: false }); if (sim.value.err) { throw new Error(`simulation failed: ${JSON.stringify(sim.value.err)}`); } return tx; // sign as-is, do not rebuild the message } ``` ## Notes - Payloads embed a recent blockhash (SVM) or current fees (bridge `value`), so they are short-lived. If verification or signing takes long, re-quote rather than patching payloads. - The `provider` and `annotation` fields describe each leg and tell you which contract and function to expect at `to` (see [Function signatures by provider](https://docs.m0.org/#function-signatures-by-provider)) or which programs to expect on Solana (see [Instructions by provider](https://docs.m0.org/#instructions-by-provider)). They set the expectation, but they are informational: anchor your checks on the decoded transaction contents and the [Deployments](https://docs.m0.org/resources/addresses) pages. # Changelog Log of notable changes added to the Protocol API. ## 2025-12-18 ### Deprecated - `hyperliquid_holder` query: Migrate to `usdhl_hyperevm_stablecoins`. See [Daily Yields](https://docs.m0.org/api-reference/protocol/recipes/daily-yields) for more information. Sunsetting by Jan 8, 2026. - `SolanaBridgeEvents` query: Migrate to `solanaSupply`. Sunsetting by Jan 8, 2026. ## 2025-12-15 ### Added - Prefixed stablecoins queries. See [Daily Yields](https://docs.m0.org/api-reference/protocol/recipes/daily-yields) for more information. ## 2025-12-08 ### Added - `MDailyEarningsSnapshots` query to get aggregated daily earnings snapshots for the entire protocol. ## 2025-10-28 ### Added - `usdz_mainnet` and `usdz_arbitrum` queries to get USDZ data from Ethereum and Arbitrum. - `dfns_mainnet` query to get 0FNS data from Ethereum. ## 2025-10-06 ### Added - `solanaUsdkSupply` query to get the daily supply of $USDK on Solana. ### Fixed - Wrapped `$M`'s `accruedYield` and `unclaimedYield` values on Ethereum. ## 2025-07-07 ### Added - $M token on-chain Arbitrum data is now available under the `arb_*` queries. - Changelog page. - Doc's recipe to query $M Supply snapshots. ## 2025-06-24 ### Fixed - `MHolderSnapshots`: some holders with large historical data were getting wrong values for recent dates. ## 2025-04-01 🍃 Initial release. # Protocol API The Protocol API is a GraphQL API for querying on-chain data: token supplies, holder balances, rewards accrual, earner status, and protocol configuration parameters. ## Endpoint ```text https://protocol-api.m0.org/graphql ``` An [interactive GraphQL playground](https://protocol-api.m0.org/graphql){rel=""nofollow""} is available at the same URL for exploring the schema and testing queries. ## Authentication The M0 Protocol API requires authentication via API keys to access its endpoint. This ensures that only authorized users can interact with the API and access its data. [Contact us](https://www.m0.org/contact-us){rel=""nofollow""} to obtain an API key. ### Using API Keys To authenticate your requests, include the API key in the `Authorization` header of your HTTP requests. For example post with curl: ```sh curl -i \ --request POST \ --header "Content-Type: application/json" \ --header "Authorization: ApiKey " \ --data '{"query":"{ __typename }"}' \ https://protocol-api.m0.org/graphql ``` API keys should be kept secret and not exposed in public repositories or client-side code. If you believe your API key has been compromised, please contact us immediately to revoke the key and issue a new one. ### Storing API Keys Securely To protect your API key and prevent unauthorized access, follow these best practices: #### Restrict Access by Origin and IP Configure your API key to limit where it can be used from: - **IP Allowlisting**: Restrict the API key to specific IP addresses or CIDR ranges that correspond to your backend servers - **Origin Restrictions**: If applicable, limit the domains or origins that can make requests using your API key Contact us to configure these restrictions for your API key. This adds an additional layer of security, ensuring that even if your key is compromised, it cannot be used from unauthorized locations. #### Use a Backend Service Consider implementing a backend service that: - Stores the API key securely in environment variables or a secrets manager - Acts as a proxy between your frontend and the M0 Protocol API - Handles authentication on behalf of your users This approach ensures that your API key is never transmitted to or accessible from the client. ## Query Recipes Common GraphQL queries are documented as ready-to-use recipes: - [Network Supply](https://docs.m0.org/api-reference/protocol/recipes/network-supply) - [Token Overview](https://docs.m0.org/api-reference/protocol/recipes/token-overview) - [Token Holders](https://docs.m0.org/api-reference/protocol/recipes/token-holders) - [Daily Yields](https://docs.m0.org/api-reference/protocol/recipes/daily-yields) - [Earner Rate History](https://docs.m0.org/api-reference/protocol/recipes/earner-rate-history) - [Minter Daily Expenses](https://docs.m0.org/api-reference/protocol/recipes/minter-daily-expenses) - [Protocol Configuration](https://docs.m0.org/api-reference/protocol/recipes/protocol-config) # Daily Yields This guide outlines how to retrieve daily yield accrual data for various stablecoins using our GraphQL API. Depending on the stablecoin, yield values can be queried as aggregated data for specific stablecoins or for individual holders. ## Aggregated Yield Data by Stablecoin For most of our stablecoins, we provide aggregated yield and supply data per chain. This includes chain-specific queries with a prefix for other stablecoins. The snippets below use **Metamask USD (mUSD)** as an example. You can find all supported stablecoins and chain prefixes below ### Aggregated data ```graphql # Fetches aggregated data for mUSD on Mainnet, Linea, and BSC query GetMusdStablecoin { mainnet: musd_mainnet_stablecoins { id claimed minted accruedYield supply lastUpdate } linea: musd_linea_stablecoins { id claimed minted accruedYield supply lastUpdate } bsc: musd_bsc_stablecoins { id claimed minted accruedYield supply lastUpdate } } ``` You can also get daily snapshots for yield and supply for these stablecoins. ### Daily Yield Snapshots ```graphql # Fetches daily yield snapshots for mUSD on Mainnet query GetMusdDailyYields { musd_mainnet_yieldStats_collection(interval: day) { id timestamp amount blockNumber } } ``` ### Daily Supply Snapshots ```graphql # Fetches daily supply snapshots for mUSD on Mainnet query GetMusdDailySupply { musd_mainnet_supplyStats_collection(interval: day) { id timestamp amount blockNumber } } ``` ::note You can find more supported queries in the interactive [GraphQL Playground documentation](https://protocol-api.m0.org/graphql){rel=""nofollow""}. :: ### Supported Stablecoins and Prefixes The following table lists the stablecoins that support prefixed queries and the corresponding prefixes for each chain. | Stablecoin | Ticker | Prefixes | | :----------- | :-------- | :---------------------------------------------- | | USDhl | USDHL | `usdhl_hyperevm_` | | Braid Dollar | USDZ | `usdz_mainnet_`, `usdz_arbitrum_` | | Dfns Rewards | 0fns | `dfns_mainnet_` | | Startale USD | USDSC | `usdsc_soneium_` | | MANTRA USD | mantraUSD | `mantrausd_mantra_` | | Metamask USD | mUSD | `musd_mainnet_`, `musd_linea_`, `musd_binance_` | | Daylight USD | GRID | `grid_plasma_` | ## Yield Snapshots on Ethereum For stablecoins like Noble Dollar (USDN), Wrapped `$M` (`$wM`), Usual (UsualM), and USDai, use the `MHolderSnapshots` and `MHolders` queries that return yield information for `$M` Earners on **Ethereum**. ### For Noble Dollar (USDN) This query returns daily snapshots of accrued yield for a given USDN holder. You can specify a starting date with the `from` parameter. If `from` is omitted, it defaults to the last 30 days. ```graphql # Fetches daily yield snapshots for a USDN holder query UsdnYieldSnapshots($address: String!, $from: String) { MHolderSnapshots(address: $address, from: $from) { balance accruedYield timestamp principal } } ``` **Example:** Get accrued yield snapshots for a USDN holder since April 1st, 2025. ```json { "address": "0x83Ae82Bd4054e815fB7B189C39D9CE670369ea16", "from": "2025-04-01" } ``` To get only the current values for USDN holders, you can use the `MHolders` query. ```graphql # Fetches current yield values for the top 100 USDN holders query GetUsdnHolders { MHolders(first: 100) { address balance accruedYield } } ``` ### For Wrapped `$M` (`$wM`) / Usual (UsualM) This query returns daily snapshots of accrued yield for a given wM holder. Like the previous query, it supports the `from` parameter and defaults to the last 30 days if omitted. ```graphql # Fetches daily yield snapshots for a wM holder query WmYieldSnapshots($address: String!, $from: String) { WMHolderSnapshots(address: $address, from: $from) { balance accruedYield timestamp principal } } ``` **Example:** Get accrued yield snapshots for a wM holder since April 1st, 2025. ```json { "address": "0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470", "from": "2025-04-01" } ``` ### For USDai on Arbitrum This query retrieves current yield values for USDai holders on Arbitrum. ```graphql # Fetches current yield values for USDai holders on Arbitrum query GetUsdaiArbitrumData { WMHoldersArbitrum { address balance accruedYield claimedYield unclaimedYield } } ``` ## Yield Snapshots on Solana Use `MSolanaYieldSnapshots` to retrieve daily yield snapshots for `$M` earners on Solana. ```graphql # Fetches daily yield snapshots for all $M earners on Solana query GetMSolanaDailyYields { MSolanaYieldSnapshots { address dayTimestamp balance accruedYield earned } } ``` The `address` and date filters are optional. For date ranges, use `day_timestamp_gte` and `day_timestamp_lte`. ```graphql # Fetches daily yield snapshots for a specific holder in a time range query GetMSolanaDailyYieldsForAddress( $address: String! $from: String! $to: String! ) { MSolanaYieldSnapshots( address: $address day_timestamp_gte: $from day_timestamp_lte: $to orderDirection: desc ) { address dayTimestamp balance accruedYield earned fromTs } } ``` All timestamps are in seconds. - `address`: is the earner's Solana address - `dayTimestamp`: timestamp for the day entry - `timestamp`: timestamp at which the `earned` value got calculated - `earned`: yield accrual between `fromTs` to `timestamp` value - `accruedYield`: total earnings **Example holder:** [USDKY `BVo3...eVjc`](https://solscan.io/account/BVo36cZqxD6KUJGhHPZvBDPbe1Q5fR7ekYDj1mbReVjc){rel=""nofollow""} ```json { "address": "BVo36cZqxD6KUJGhHPZvBDPbe1Q5fR7ekYDj1mbReVjc", "from": "2025-01-01", "to": "2025-01-31" } ``` ### Supported Solana earners Currently, the query above will provide information for the following earners | Earner | Vault's token account | | :----- | :------------------------------------------- | | USDKY | BVo36cZqxD6KUJGhHPZvBDPbe1Q5fR7ekYDj1mbReVjc | | USDK | EHNaRY1ZdtaoPVMqE3TW6pacACzEoU1e9V1ToLyavowN | | wM | 7upNeuSPSpinN7zzEsrxMe6p3N6tMub67dkkm5LFBTvp | | USD+ | 67GWo9en5KtPfeZn5LWrCyhL2w31rf1oqeCoszPUKsAs | | XO | GeBTfYujk4JLr2MBCZznMTLTGch1QrUyYcMKen4KUFU2 | # Earner Rate History This query will return snapshots of the `$M` earner rate ```graphql query EarnerRateSnapshotsQuery { rates: mToken_latestRateSnapshots( orderDirection: desc orderBy: timestamp first: 4 ) { value timestamp } } ``` Example output: ```json { "data": { "rates": [ { "value": "407", "timestamp": "1747235135" }, { "value": "415", "timestamp": "1747223375" }, { "value": "415", "timestamp": "1747221779" }, { "value": "415", "timestamp": "1747215359" } ] } } ``` # Minter Daily Expenses This query will return the daily expenses of the Minter identified by `address`. The date range starts at `from` until the current time. ```graphql query MinterExpensesQuery($address: String!, $from: String) { MinterExpenses(address: $address, from: $from) { amount timestamp date: timestampAsDate } } ``` ## Example Get the Minter expenses for MXON `0x7F7489582b64ABe46c074A45d758d701c2CA5446` since January 1st, 2025. variables: ```json { "address": "0x7F7489582b64ABe46c074A45d758d701c2CA5446", "from": "01 Jan 2025" } ``` When `from` is not specified, it defaults to the last 30 days. # Network Supply Use this query to get the current network supply and the reserves (collateral) of the M0 Protocol. ```graphql query M0NetworkSupplyQuery { supply: totalOwedMs(orderBy: blockTimestamp, orderDirection: desc, first: 1) { amount lastUpdate: blockTimestamp } reserves: minterGateway_totalCollateralSnapshots( orderBy: timestamp orderDirection: desc first: 1 ) { amount: value lastUpdate: timestamp } } ``` Example output: ```json { "data": { "supply": [ { "amount": "261403752633708", "lastUpdate": "1787231531" } ], "reserves": [ { "amount": "266219796632317", "lastUpdate": "1787223731" } ] } } ``` ## Issuers and reserves For detailed breakdown of reserves by issuer, you can use the following query: ```graphql query M0ReservesByMinterQuery { issuers: minterGateway_minters { address isActive activationTimestamp collateral minted burned } } ``` # Protocol Configuration Get historical values for protocol configuration. Current values are featured in the [Governance app](https://governance.m0.org/config/protocol){rel=""nofollow""} too. The configuration includes the following keys: - `update_collateral_interval` - `update_collateral_threshold` - `penalty_rate` - `mint_delay` - `mint_ttl` - `mint_ratio` - `minter_freeze_time` - `base_minter_rate` - `max_earner_rate` - `guidance` - `minter_rate_model` - `earner_rate_model` To query them all: ```graphql query ProtocolConfigQuery { protocolConfigs { id value key blockTimestamp } } ``` Example output: ```json { "data": { "protocolConfigs": [ { "id": "0x25b657dfec5d4c889e0b4050224deaaa00c3707590a7e46c536d6218d673078846010000", "value": "108000", "key": "update_collateral_interval", "blockTimestamp": "1715827991" }, { "id": "0x2637ef87d12653c1d02fff79ad1de0ead33bf13e6f8c475b5efb15bc8547f74ea1000000", "value": "5", "key": "penalty_rate", "blockTimestamp": "1715828219" }, { "id": "0x268e7cf3cfe0d5c4e8b8b62cbeb8ae36497cb7c3f3f75d2d788150e1c97b407d2a010000", "value": "10800", "key": "mint_ttl", "blockTimestamp": "1715828387" } ] } } ``` Use filters to narrow down data. E.g., filter by `max_earner_rate` ```graphql query ProtocolConfigQuery { protocolConfigs(where: { key: "max_earner_rate" }) { id value key blockTimestamp } } ``` # Token Holders This guide explains how to query holder information across different chains in the M0 ecosystem. Each chain features M0-powered stablecoins that are included in the holders response. ## Ethereum Query current `$M` holders on Ethereum with their balances and accrued yields: ```graphql query MHoldersQuery { MHolders(first: 100) { address balance accruedYield } } ``` ### Featured Stablecoins on Ethereum The holders response includes the following M0-powered stablecoins: - **`$M` (Wrapped)** (WM) - `0x437cc33344a0b27a429f795ff6b469c72698b291` - **Metamask USD** (mUSD) - `0xacA92E438df0B2401fF60dA7E4337B687a2435DA` - **Noble** (USDN) - `0x83Ae82Bd4054e815fB7B189C39D9CE670369ea16` - **Usual** (UsualM) - `0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470` ## Arbitrum Query current `$M` holders on Arbitrum with their balances and accrued yields: ```graphql query WMHoldersArbitrumQuery { WMHoldersArbitrum(first: 100) { address balance accruedYield claimedYield unclaimedYield } } ``` ### Featured Stablecoins on Arbitrum The holders response includes the following M0-powered stablecoins: - **USDai** (USDai) - `0x83Ae82Bd4054e815fB7B189C39D9CE670369ea16` ## Hyperliquid Query a specific `$M` holder on Hyperliquid: ```graphql query HyperliquidHolderQuery { hyperliquid_holder { address isEarning accruedYield } } ``` ### Featured Stablecoins on Hyperliquid The holders response includes the following M0-powered stablecoins: - USDhl (USDhl) - `0xb50A96253aBDF803D85efcDce07Ad8becBc52BD5` ## Linea Query a specific `$M` holder on Linea: ```graphql query LineaHolderQuery { linea_holders { address isEarning accruedYield } } ``` ### Featured Stablecoins on Linea The holders response includes the following M0-powered stablecoins: - **Metamask USD** (mUSD) - `0xacA92E438df0B2401fF60dA7E4337B687a2435DA` ## Solana Coming soon. # Token Overview This guide explains how to retrieve token data for M0 protocol tokens. The API provides metadata, holders, and supply information for multiple tokens including: - **`$M`** - **POWER** - **ZERO** ## Query Multiple Tokens Use the `tokens` query to retrieve metadata, supply data, and holder information for all tokens: ```graphql query TokensOverview { tokens { id name symbol decimals totalSupplys(first: 2, orderBy: blockTimestamp, orderDirection: desc) { value blockTimestamp } holders(first: 10, orderBy: balance, orderDirection: desc) { address balance } } } ``` This query returns: - Token metadata (name, symbol, decimals, contract address) - Historical total supply snapshots - Top token holders and their balances Use the `token` query to retrieve data for a specific token: ```graphql query TokenOverview($id: String!) { token(id: $id) { id name symbol decimals } } ``` ## Important: Indexer Data Lag The supply data returned by the indexer represents the last-seen state and may not reflect the absolute latest on-chain data. There can be a delay between when a transaction occurs on-chain and when the indexer processes it. ## Querying Supply Directly from Chain For the most up-to-date supply data, query the blockchain directly via RPC: ```typescript // Using viem, ethers.js or web3.js const totalSupply = await tokenContract.totalSupply(); ``` Note that the method to retrieve the total supply may vary between contracts. Before querying the blockchain directly, review the contract's ABI and documentation to ensure you're using the correct method. You can find our token contracts on [Deployments](https://docs.m0.org/resources/addresses). # Deployments ::card-group :::card --- icon: i-lucide-coins title: M0 Platform to: https://docs.m0.org/resources/addresses/m0-platform --- Contract addresses for the M0 platform, featuring the M0 token, TTG governance, Wrapped M, and stablecoin extensions across all supported chains. ::: :::card --- icon: i-lucide-coins title: PYUSDx Platform to: https://docs.m0.org/resources/addresses/pyusdx-platform --- Contract addresses for the PYUSDx platform, featuring a yield-bearing, non-rebasing token backed by PYUSD and issued by MoonPay, along with stablecoin extensions across all supported chains. ::: :::card --- icon: i-lucide-coins title: MGUSD to: https://docs.m0.org/resources/addresses/mgusd-platform --- Contract addresses for MGUSD, an M0-powered Stellar Asset Contract (SAC) stablecoin, administered by a Soroban Minter Gateway contract. ::: :: --- ## API Reference ::card-group :::card --- icon: i-lucide-code title: API Reference to: https://docs.m0.org/api-reference/introduction --- Explore the M0 API for querying protocol data, token metrics, and on-chain activity. ::: :: # M0 Platform Deployments The M0 platform and TTG were deployed to Ethereum Mainnet on May 7th 2024. Wrapped M was deployed to Ethereum Mainnet on Aug 14th 2024. ## Ethereum | Contract | Address | ABI | Repository | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://etherscan.io/address/0x866a2bf4e572cbcf37d5071a7a58503bfb36be1b){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x866a2bf4e572cbcf37d5071a7a58503bfb36be1b?tab=contract-viewer#code){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://etherscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer#code){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | MinterGateway | [0xf7f9638cb444D65e5A40bF5ff98ebE4ff319F04E](https://etherscan.io/address/0xf7f9638cb444D65e5A40bF5ff98ebE4ff319F04E){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xf7f9638cb444D65e5A40bF5ff98ebE4ff319F04E?tab=contract-viewer#code){rel=""nofollow""} | [MinterGateway](https://github.com/m0-foundation/protocol/blob/main/src/MinterGateway.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://etherscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract-viewer#code){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | StandardGovernor | [0xB024aC5a7c6bC92fbACc8C3387E628a07e1Da016](https://etherscan.io/address/0xB024aC5a7c6bC92fbACc8C3387E628a07e1Da016){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xB024aC5a7c6bC92fbACc8C3387E628a07e1Da016?tab=contract-viewer#code){rel=""nofollow""} | [StandardGovernor](https://github.com/m0-foundation/ttg/blob/main/src/StandardGovernor.sol){rel=""nofollow""} | | EmergencyGovernor | [0x886d405949F709bC3f4451491bDd07ff51Cdf90A](https://etherscan.io/address/0x886d405949F709bC3f4451491bDd07ff51Cdf90A){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x886d405949F709bC3f4451491bDd07ff51Cdf90A?tab=contract-viewer#code){rel=""nofollow""} | [EmergencyGovernor](https://github.com/m0-foundation/ttg/blob/main/src/EmergencyGovernor.sol){rel=""nofollow""} | | ZeroGovernor | [0xa0DAFaEEA4A1d44534e1b9227e19CAE6358b80FE](https://etherscan.io/address/0xa0DAFaEEA4A1d44534e1b9227e19CAE6358b80FE){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xa0DAFaEEA4A1d44534e1b9227e19CAE6358b80FE?tab=contract-viewer#code){rel=""nofollow""} | [ZeroGovernor](https://github.com/m0-foundation/ttg/blob/main/src/ZeroGovernor.sol){rel=""nofollow""} | | POWER Token | [0x5983B89FA184f14917013B9C3062afD9434C5b03](https://etherscan.io/address/0x5983B89FA184f14917013B9C3062afD9434C5b03){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x5983B89FA184f14917013B9C3062afD9434C5b03?tab=contract-viewer#code){rel=""nofollow""} | [PowerToken](https://github.com/m0-foundation/ttg/blob/main/src/PowerToken.sol){rel=""nofollow""} | | ZERO Token | [0x988567FE094570cCE1FFdA29D1f2d842B70492be](https://etherscan.io/address/0x988567FE094570cCE1FFdA29D1f2d842B70492be){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x988567FE094570cCE1FFdA29D1f2d842B70492be?tab=contract-viewer#code){rel=""nofollow""} | [ZeroToken](https://github.com/m0-foundation/ttg/blob/main/src/ZeroToken.sol){rel=""nofollow""} | | DistributionVault | [0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f](https://etherscan.io/address/0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f?tab=contract-viewer#code){rel=""nofollow""} | [DistributionVault](https://github.com/m0-foundation/ttg/blob/main/src/DistributionVault.sol){rel=""nofollow""} | | Hub Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://etherscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer#code){rel=""nofollow""} | [HubPortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/HubPortal.sol){rel=""nofollow""} | | Wormhole Bridge Adapter | [0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd](https://etherscan.io/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd?tab=contract-viewer#code){rel=""nofollow""} | [WormholeBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/wormhole/WormholeBridgeAdapter.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://etherscan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer#code){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://etherscan.io/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract-viewer#code){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://etherscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer#code){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | `OrderBook` | [0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4](https://etherscan.io/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4?tab=contract-viewer#code){rel=""nofollow""} | [OrderBook](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} | ## Arbitrum | Contract | Address | ABI | Repository | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://arbiscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer#code){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://arbiscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer#code){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://arbiscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract-viewer#code){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://arbiscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer#code){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Wormhole Bridge Adapter | [0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd](https://arbiscan.io/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd?tab=contract-viewer#code){rel=""nofollow""} | [WormholeBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/wormhole/WormholeBridgeAdapter.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://arbiscan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer#code){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://arbiscan.io/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract-viewer#code){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://arbiscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer#code){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | `OrderBook` | [0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4](https://arbiscan.io/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4?tab=contract-viewer#code){rel=""nofollow""} | [OrderBook](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} | ## Base | Contract | Address | ABI | Repository | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://basescan.org/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://basescan.org/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer#code){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://basescan.org/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://basescan.org/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer#code){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://basescan.org/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://basescan.org/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract-viewer#code){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://basescan.org/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://basescan.org/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer#code){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Wormhole Bridge Adapter | [0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd](https://basescan.org/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd){rel=""nofollow""} | [ABI](https://basescan.org/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd?tab=contract-viewer#code){rel=""nofollow""} | [WormholeBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/wormhole/WormholeBridgeAdapter.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://basescan.org/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://basescan.org/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer#code){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://basescan.org/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://basescan.org/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract-viewer#code){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://basescan.org/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://basescan.org/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer#code){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | `OrderBook` | [0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4](https://basescan.org/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4){rel=""nofollow""} | [ABI](https://basescan.org/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4?tab=contract-viewer#code){rel=""nofollow""} | [OrderBook](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} | ## Linea | Contract | Address | ABI | Repository | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://lineascan.build/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer#code){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://lineascan.build/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer#code){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x9c55E091C53faA16F0d05322f6FE2F728E2b8491](https://lineascan.build/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491?tab=contract-viewer#code){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://lineascan.build/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://lineascan.build/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer#code){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://lineascan.build/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://lineascan.build/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer#code){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://lineascan.build/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://lineascan.build/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer#code){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## BNB | Contract | Address | ABI | Repository | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://bscscan.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer#code){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://bscscan.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer#code){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x9c55E091C53faA16F0d05322f6FE2F728E2b8491](https://bscscan.com/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491?tab=contract-viewer#code){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://bscscan.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://bscscan.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer#code){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://bscscan.com/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://bscscan.com/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer#code){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://bscscan.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://bscscan.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer#code){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## HyperEVM | Contract | Address | ABI | Repository | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://hyperevmscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer#code){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://hyperevmscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer#code){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x9c55E091C53faA16F0d05322f6FE2F728E2b8491](https://hyperevmscan.io/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491?tab=contract-viewer#code){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://hyperevmscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer#code){rel=""nofollow""} | [Portal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://hyperevmscan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer#code){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://hyperevmscan.io/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract-viewer#code){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://hyperevmscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer#code){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Mantra | Contract | Address | ABI | Repository | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://blockscout.mantrascan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://blockscout.mantrascan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x9c55E091C53faA16F0d05322f6FE2F728E2b8491](https://blockscout.mantrascan.io/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491?tab=contract){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://blockscout.mantrascan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://blockscout.mantrascan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://blockscout.mantrascan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Soneium | Contract | Address | ABI | Repository | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://soneium.blockscout.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://soneium.blockscout.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x9c55E091C53faA16F0d05322f6FE2F728E2b8491](https://soneium.blockscout.com/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491?tab=contract){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://soneium.blockscout.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://soneium.blockscout.com/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://soneium.blockscout.com/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://soneium.blockscout.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | Uniswap V3 Adapter | [0x023bd2F0A95373C55FC8D1c5F8e60cC3B9Bc4f4b](https://soneium.blockscout.com/address/0x023bd2F0A95373C55FC8D1c5F8e60cC3B9Bc4f4b){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x023bd2F0A95373C55FC8D1c5F8e60cC3B9Bc4f4b?tab=contract){rel=""nofollow""} | | ## Plasma | Contract | Address | ABI | Repository | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://plasmascan.to/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://plasmascan.to/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x9c55E091C53faA16F0d05322f6FE2F728E2b8491](https://plasmascan.to/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491#code){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://plasmascan.to/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd#code){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://plasmascan.to/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32#code){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://plasmascan.to/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d#code){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://plasmascan.to/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Citrea | Contract | Address | ABI | Repository | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://explorer.mainnet.citrea.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://explorer.mainnet.citrea.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x9c55E091C53faA16F0d05322f6FE2F728E2b8491](https://explorer.mainnet.citrea.xyz/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x9c55E091C53faA16F0d05322f6FE2F728E2b8491?tab=contract){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://explorer.mainnet.citrea.xyz/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://explorer.mainnet.citrea.xyz/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://explorer.mainnet.citrea.xyz/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://explorer.mainnet.citrea.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | `OrderBook` | [0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4](https://explorer.mainnet.citrea.xyz/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4?tab=contract){rel=""nofollow""} | [OrderBook](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} | ## 0G | Contract | Address | ABI | Repository | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://chainscan.0g.ai/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://chainscan.0g.ai/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://chainscan.0g.ai/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract-viewer){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://chainscan.0g.ai/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Wormhole Bridge Adapter | [0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd](https://chainscan.0g.ai/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd?tab=contract-viewer){rel=""nofollow""} | [WormholeBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/wormhole/WormholeBridgeAdapter.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://chainscan.0g.ai/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://chainscan.0g.ai/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract-viewer){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://chainscan.0g.ai/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Fluent | Contract | Address | ABI | Repository | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://fluentscan.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://fluentscan.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://fluentscan.xyz/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://fluentscan.xyz/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://fluentscan.xyz/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://fluentscan.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Moca | Contract | Address | ABI | Repository | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://scan.mocachain.org/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://scan.mocachain.org/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://scan.mocachain.org/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://scan.mocachain.org/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://scan.mocachain.org/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://scan.mocachain.org/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://scan.mocachain.org/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://scan.mocachain.org/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Wormhole Bridge Adapter | [0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd](https://scan.mocachain.org/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd){rel=""nofollow""} | [ABI](https://scan.mocachain.org/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd?tab=contract){rel=""nofollow""} | [WormholeBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/wormhole/WormholeBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://scan.mocachain.org/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://scan.mocachain.org/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | `OrderBook` | [0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4](https://scan.mocachain.org/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4){rel=""nofollow""} | [ABI](https://scan.mocachain.org/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4?tab=contract){rel=""nofollow""} | [OrderBook](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} | ## Monad | Contract | Address | ABI | Repository | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://monadvision.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://monadvision.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=Contract#contract-abi){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://monadvision.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://monadvision.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=Contract#contract-abi){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://monadvision.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://monadvision.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=Contract#contract-abi){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://monadvision.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://monadvision.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=Contract#contract-abi){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Wormhole Bridge Adapter | [0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd](https://monadvision.com/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd){rel=""nofollow""} | [ABI](https://monadvision.com/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd?tab=Contract#contract-abi){rel=""nofollow""} | [WormholeBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/wormhole/WormholeBridgeAdapter.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://monadvision.com/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://monadvision.com/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=Contract#contract-abi){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://monadvision.com/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://monadvision.com/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=Contract#contract-abi){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://monadvision.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://monadvision.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=Contract#contract-abi){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | `OrderBook` | [0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4](https://monadvision.com/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4){rel=""nofollow""} | [ABI](https://monadvision.com/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4?tab=Contract#contract-abi){rel=""nofollow""} | [OrderBook](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} | ## Rise | Contract | Address | ABI | Repository | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://explorer.risechain.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract_abi){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://explorer.risechain.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract_abi){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://explorer.risechain.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract_abi){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://explorer.risechain.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract_abi){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | LayerZero Bridge Adapter | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://explorer.risechain.com/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract_abi){rel=""nofollow""} | [LayerZeroBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/layerZero/LayerZeroBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://explorer.risechain.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract_abi){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | `OrderBook` | [0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4](https://explorer.risechain.com/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0xe39B012AB3b20E94a9beEa557eB0DE4171D4D3E4?tab=contract_abi){rel=""nofollow""} | [OrderBook](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} | ## Nexus | Contract | Address | ABI | Repository | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://explorer.nexus.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://explorer.nexus.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://explorer.nexus.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://explorer.nexus.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://explorer.nexus.xyz/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://explorer.nexus.xyz/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://explorer.nexus.xyz/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://explorer.nexus.xyz/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/SpokePortal.sol){rel=""nofollow""} | | Hyperlane Bridge Adapter | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://explorer.nexus.xyz/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://explorer.nexus.xyz/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract){rel=""nofollow""} | [HyperlaneBridgeAdapter](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/src/bridgeAdapters/hyperlane/HyperlaneBridgeAdapter.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://explorer.nexus.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://explorer.nexus.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Solana ### Mainnet | Program/Token | Address | Explorer | Repository | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | M Token | [mzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJo](https://explorer.solana.com/address/mzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJo){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJo){rel=""nofollow""} | [solana-m](https://github.com/m0-foundation/solana-m){rel=""nofollow""} | | Wrapped M Token | [mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp](https://explorer.solana.com/address/mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp){rel=""nofollow""} | [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} | | Portal | [mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY](https://explorer.solana.com/address/mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY){rel=""nofollow""} | | | Transceiver | [J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm](https://explorer.solana.com/address/J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm){rel=""nofollow""} | [View](https://explorer.solana.com/address/J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm){rel=""nofollow""} | | | Quoter | [Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ](https://explorer.solana.com/address/Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ){rel=""nofollow""} | [View](https://explorer.solana.com/address/Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ){rel=""nofollow""} | | | `order_book` | [MzLoYnJ6sF6eeejs4vV95TNmXqS3W4cAtLGKkjT4ZrK](https://explorer.solana.com/address/MzLoYnJ6sF6eeejs4vV95TNmXqS3W4cAtLGKkjT4ZrK){rel=""nofollow""} | [View](https://explorer.solana.com/address/MzLoYnJ6sF6eeejs4vV95TNmXqS3W4cAtLGKkjT4ZrK){rel=""nofollow""} | [liquidity-delivery](https://github.com/m0-foundation/liquidity-delivery){rel=""nofollow""} | ### Devnet | Program/Token | Address | Explorer | Repository | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | M Token | [mzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6](https://explorer.solana.com/address/mzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6?cluster=devnet){rel=""nofollow""} | [solana-m](https://github.com/m0-foundation/solana-m){rel=""nofollow""} | | Wrapped M Token | [mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp](https://explorer.solana.com/address/mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp?cluster=devnet){rel=""nofollow""} | [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} | | Portal | [mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY](https://explorer.solana.com/address/mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY?cluster=devnet){rel=""nofollow""} | | | Transceiver | [J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm](https://explorer.solana.com/address/J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm?cluster=devnet){rel=""nofollow""} | | | Quoter | [Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ](https://explorer.solana.com/address/Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ?cluster=devnet){rel=""nofollow""} | | --- ## API Reference ::card-group :::card --- icon: i-lucide-code title: API Reference to: https://docs.m0.org/api-reference/introduction --- Explore the M0 API for querying protocol data, token metrics, and on-chain activity. ::: :: # MGUSD Deployments MGUSD is an M0-powered Stellar Asset Contract (SAC) stablecoin. The **Minter Gateway** is a Soroban contract that administers the **Stellar Asset Contract (SAC)** holding MGUSD balances. For a technical overview, see [MGUSD on Stellar](https://docs.m0.org/protocol/stellar). ::note Deployment addresses will be published here once the contracts are live. Check back, or reach out to the M0 team for the latest. :: ## Mainnet | Contract | Address | Explorer | Repository | | -------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | MGUSD Minter Gateway | `CD7LJGITEYTOAVKH3PPAVOBWXLMMFGK3NN5W4QAXFJLNSNS4R46TW562` | [View](https://stellar.expert/explorer/public/contract/CD7LJGITEYTOAVKH3PPAVOBWXLMMFGK3NN5W4QAXFJLNSNS4R46TW562){rel=""nofollow""} | [mgusd-minter-gateway](https://github.com/m0-platform/mgusd-minter-gateway){rel=""nofollow""} | | MGUSD SAC | `CDK2LDSYUKPEFN3HNE7K7ETUT3VIOBHSOXAK5CTO4A4RKKZQUCAIWCJA` | [View](https://stellar.expert/explorer/public/contract/CDK2LDSYUKPEFN3HNE7K7ETUT3VIOBHSOXAK5CTO4A4RKKZQUCAIWCJA){rel=""nofollow""} | — | ## Testnet On testnet the asset code is **TMGUSD**. | Contract | Address | Explorer | Repository | | -------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | MGUSD Minter Gateway | `CCPGOJYZAJ4H5JW2WFN2C5O4SC6DBJB7QD4ACQ2A3GUMKFMYNXLMYJME` | [View](https://stellar.expert/explorer/testnet/contract/CCPGOJYZAJ4H5JW2WFN2C5O4SC6DBJB7QD4ACQ2A3GUMKFMYNXLMYJME){rel=""nofollow""} | [mgusd-minter-gateway](https://github.com/m0-platform/mgusd-minter-gateway){rel=""nofollow""} | | MGUSD SAC | `CCWJCHDLXEIMXODO5JFZLRUM7AMA7EI2NBRBL44ROCL6WH44W22NM7HM` | [View](https://stellar.expert/explorer/testnet/contract/CCWJCHDLXEIMXODO5JFZLRUM7AMA7EI2NBRBL44ROCL6WH44W22NM7HM){rel=""nofollow""} | — | # PYUSDx Platform Deployments The PYUSDx platform was deployed to Ethereum and Arbitrum Mainnet on June 2nd 2026, to Monad Mainnet on August 12th 2026, and to Base Mainnet on September 3rd 2026. Proxy addresses are CREATE2-deterministic, so each contract has the same address on every chain. ## Mainnet ### Ethereum | Contract | Address | ABI | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | PYUSDx Token | [0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d](https://etherscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d#code#L1){rel=""nofollow""} | | IssuerGateway | [0x693CC3305342B02AC1549B509a704ff944Cd9499](https://etherscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499#code#L1){rel=""nofollow""} | | Portal | [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://etherscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code#L1){rel=""nofollow""} | | SwapFacility | [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://etherscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code#L1){rel=""nofollow""} | | ExtensionFactory | [0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A](https://etherscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A#code#L1){rel=""nofollow""} | | LayerZero Bridge Adapter | [0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D](https://etherscan.io/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D#code#L1){rel=""nofollow""} | ### Arbitrum | Contract | Address | ABI | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | PYUSDx Token | [0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d](https://arbiscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d#code#L1){rel=""nofollow""} | | IssuerGateway | [0x693CC3305342B02AC1549B509a704ff944Cd9499](https://arbiscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499#code#L1){rel=""nofollow""} | | Portal | [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://arbiscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code#L1){rel=""nofollow""} | | SwapFacility | [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://arbiscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code#L1){rel=""nofollow""} | | ExtensionFactory | [0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A](https://arbiscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A#code#L1){rel=""nofollow""} | | LayerZero Bridge Adapter | [0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D](https://arbiscan.io/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D#code#L1){rel=""nofollow""} | ### Monad | Contract | Address | ABI | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | PYUSDx Token | [0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d](https://monadscan.com/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | [ABI](https://monadscan.com/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d#code#L1){rel=""nofollow""} | | IssuerGateway | [0x693CC3305342B02AC1549B509a704ff944Cd9499](https://monadscan.com/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | [ABI](https://monadscan.com/address/0x693CC3305342B02AC1549B509a704ff944Cd9499#code#L1){rel=""nofollow""} | | Portal | [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://monadscan.com/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | [ABI](https://monadscan.com/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code#L1){rel=""nofollow""} | | SwapFacility | [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://monadscan.com/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | [ABI](https://monadscan.com/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code#L1){rel=""nofollow""} | | ExtensionFactory | [0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A](https://monadscan.com/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | [ABI](https://monadscan.com/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A#code#L1){rel=""nofollow""} | | LayerZero Bridge Adapter | [0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D](https://monadscan.com/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D){rel=""nofollow""} | [ABI](https://monadscan.com/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D#code#L1){rel=""nofollow""} | ### Base | Contract | Address | ABI | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | PYUSDx Token | [0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d](https://basescan.org/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | [ABI](https://basescan.org/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d#code#L1){rel=""nofollow""} | | IssuerGateway | [0x693CC3305342B02AC1549B509a704ff944Cd9499](https://basescan.org/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | [ABI](https://basescan.org/address/0x693CC3305342B02AC1549B509a704ff944Cd9499#code#L1){rel=""nofollow""} | | Portal | [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://basescan.org/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | [ABI](https://basescan.org/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code#L1){rel=""nofollow""} | | SwapFacility | [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://basescan.org/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | [ABI](https://basescan.org/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code#L1){rel=""nofollow""} | | ExtensionFactory | [0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A](https://basescan.org/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | [ABI](https://basescan.org/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A#code#L1){rel=""nofollow""} | | LayerZero Bridge Adapter | [0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D](https://basescan.org/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D){rel=""nofollow""} | [ABI](https://basescan.org/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D#code#L1){rel=""nofollow""} | ## Testnet ### Ethereum Sepolia | Contract | Address | ABI | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | PYUSDx Token | [0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d](https://sepolia.etherscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | [ABI](https://sepolia.etherscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d#code#L1){rel=""nofollow""} | | IssuerGateway | [0x693CC3305342B02AC1549B509a704ff944Cd9499](https://sepolia.etherscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | [ABI](https://sepolia.etherscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499#code#L1){rel=""nofollow""} | | Portal | [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://sepolia.etherscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | [ABI](https://sepolia.etherscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code#L1){rel=""nofollow""} | | SwapFacility | [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://sepolia.etherscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | [ABI](https://sepolia.etherscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code#L1){rel=""nofollow""} | | ExtensionFactory | [0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A](https://sepolia.etherscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | [ABI](https://sepolia.etherscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A#code#L1){rel=""nofollow""} | | LayerZero Bridge Adapter | [0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D](https://sepolia.etherscan.io/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D){rel=""nofollow""} | [ABI](https://sepolia.etherscan.io/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D#code#L1){rel=""nofollow""} | | PYUSDx Faucet | [0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26](https://sepolia.etherscan.io/address/0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26){rel=""nofollow""} | [ABI](https://sepolia.etherscan.io/address/0x17c4DE4e1f3ba848F447151635E1BC2e120bEE26#code#L1){rel=""nofollow""} | ### Arbitrum Sepolia | Contract | Address | ABI | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | PYUSDx Token | [0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d](https://sepolia.arbiscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | [ABI](https://sepolia.arbiscan.io/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d#code#L1){rel=""nofollow""} | | IssuerGateway | [0x693CC3305342B02AC1549B509a704ff944Cd9499](https://sepolia.arbiscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | [ABI](https://sepolia.arbiscan.io/address/0x693CC3305342B02AC1549B509a704ff944Cd9499#code#L1){rel=""nofollow""} | | Portal | [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://sepolia.arbiscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | [ABI](https://sepolia.arbiscan.io/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code#L1){rel=""nofollow""} | | SwapFacility | [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://sepolia.arbiscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | [ABI](https://sepolia.arbiscan.io/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code#L1){rel=""nofollow""} | | ExtensionFactory | [0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A](https://sepolia.arbiscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | [ABI](https://sepolia.arbiscan.io/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A#code#L1){rel=""nofollow""} | | LayerZero Bridge Adapter | [0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D](https://sepolia.arbiscan.io/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D){rel=""nofollow""} | [ABI](https://sepolia.arbiscan.io/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D#code#L1){rel=""nofollow""} | | PYUSDx Faucet | [0x733183C4b50d4426B1118CC1b412EF7Eb6410bF8](https://sepolia.arbiscan.io/address/0x733183C4b50d4426B1118CC1b412EF7Eb6410bF8){rel=""nofollow""} | [ABI](https://sepolia.arbiscan.io/address/0x733183C4b50d4426B1118CC1b412EF7Eb6410bF8#code#L1){rel=""nofollow""} | ### Monad Testnet | Contract | Address | ABI | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | PYUSDx Token | [0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d](https://testnet.monadscan.com/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | [ABI](https://testnet.monadscan.com/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d#code#L1){rel=""nofollow""} | | IssuerGateway | [0x693CC3305342B02AC1549B509a704ff944Cd9499](https://testnet.monadscan.com/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | [ABI](https://testnet.monadscan.com/address/0x693CC3305342B02AC1549B509a704ff944Cd9499#code#L1){rel=""nofollow""} | | Portal | [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://testnet.monadscan.com/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | [ABI](https://testnet.monadscan.com/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code#L1){rel=""nofollow""} | | SwapFacility | [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://testnet.monadscan.com/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | [ABI](https://testnet.monadscan.com/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code#L1){rel=""nofollow""} | | ExtensionFactory | [0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A](https://testnet.monadscan.com/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | [ABI](https://testnet.monadscan.com/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A#code#L1){rel=""nofollow""} | | LayerZero Bridge Adapter | [0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D](https://testnet.monadscan.com/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D){rel=""nofollow""} | [ABI](https://testnet.monadscan.com/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D#code#L1){rel=""nofollow""} | ### Base Sepolia | Contract | Address | ABI | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | PYUSDx Token | [0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d](https://sepolia.basescan.org/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d){rel=""nofollow""} | [ABI](https://sepolia.basescan.org/address/0xeBDB0942cE16386Ab90718C7BD10C91CDb66b14d#code#L1){rel=""nofollow""} | | IssuerGateway | [0x693CC3305342B02AC1549B509a704ff944Cd9499](https://sepolia.basescan.org/address/0x693CC3305342B02AC1549B509a704ff944Cd9499){rel=""nofollow""} | [ABI](https://sepolia.basescan.org/address/0x693CC3305342B02AC1549B509a704ff944Cd9499#code#L1){rel=""nofollow""} | | Portal | [0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7](https://sepolia.basescan.org/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7){rel=""nofollow""} | [ABI](https://sepolia.basescan.org/address/0xaAD1466fE33d373189FB9dcC47270e608FeEE8A7#code#L1){rel=""nofollow""} | | SwapFacility | [0x0bC305e7e13113cAEd3f5486849e9518a1cC4173](https://sepolia.basescan.org/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173){rel=""nofollow""} | [ABI](https://sepolia.basescan.org/address/0x0bC305e7e13113cAEd3f5486849e9518a1cC4173#code#L1){rel=""nofollow""} | | ExtensionFactory | [0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A](https://sepolia.basescan.org/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A){rel=""nofollow""} | [ABI](https://sepolia.basescan.org/address/0x25c8aFfC5a63D8E047c12918C0438ABA5aA09c2A#code#L1){rel=""nofollow""} | | LayerZero Bridge Adapter | [0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D](https://sepolia.basescan.org/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D){rel=""nofollow""} | [ABI](https://sepolia.basescan.org/address/0xEfF09B0C726789F4C123397C04F5Ed4a9A20070D#code#L1){rel=""nofollow""} | | PYUSDx Faucet | [0x873b9C5a55e67C433914917210059749C0FEFf4A](https://sepolia.basescan.org/address/0x873b9C5a55e67C433914917210059749C0FEFf4A){rel=""nofollow""} | [ABI](https://sepolia.basescan.org/address/0x873b9C5a55e67C433914917210059749C0FEFf4A#code#L1){rel=""nofollow""} | No PYUSDx faucet is deployed on Monad Testnet. Bridge testnet PYUSDx from Sepolia through the `Portal` instead. # Audits ## M0 Platform and TTG Smart Contracts The M0 platform and TTG smart contracts went through a series of extensive audits by industry-leading security companies. | Auditor | Date | Report | | --------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Quantstamp | Jan 2024 - March 2024 | [Quantstamp\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Quantstamp%20Audit%20Report.pdf){rel=""nofollow""} | | Three Sigma | Jan 2024 - March 2024 | [ThreeSigma\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/ThreeSigma%20Audit%20Report.pdf){rel=""nofollow""} | | Certora | Jan 2024 - March 2024 | [Certora\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Certora%20Audit%20report.pdf){rel=""nofollow""} | | Chainsecurity | Jan 2024 - March 2024 | [Chainsecurity\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/ChainSecurity%20Audit%20Report.pdf){rel=""nofollow""} | | OpenZeppelin | Jan 2024 - March 2024 | [OpenZeppelin report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/OpenZeppelin%20Audit%20Report.pdf){rel=""nofollow""} | | Prototech Labs | Jan 2024 - Feb 2024 | [PrototechLabs\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Prototech%20Labs%20Audit%20Report.pdf){rel=""nofollow""} | | Kirill Fedoseev | Dec 2023 - April 2024 | [Kirill Fedoseev report.md](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Kirill%20Fedoseev%20Independent%20Auditor%20Report.md){rel=""nofollow""} | | Sherlock | March 2024 - April 2024 | [Sherlock\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Sherlock%20Audit%20Report.pdf){rel=""nofollow""} | ## Wrapped M (wM) Wrapped M (wM) went through a series of audits in summer 2024. | Auditor | Date | Report | | --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Chainsecurity | July 2024 - Aug 2024 | [ChainSecurity Wrapped `$M` Audit Report.pdf](https://github.com/m0-foundation/documentation/blob/main/wrapped-M-audit-reports/ChainSecurity%20Wrapped%20M%20Audit%20Report.pdf){rel=""nofollow""} | | Three Sigma | July 2024 - Aug 2024 | [Three Sigma Wrapped `$M` Audit Report.pdf](https://github.com/m0-foundation/documentation/blob/main/wrapped-M-audit-reports/ThreeSigma%20Wrapped%20M%20Audit%20Report.pdf){rel=""nofollow""} | | Kirill Fedoseev | July 2024 - Aug 2024 | [Kirill Fedoseev Wrapped `$M` Audit Report.md](https://github.com/m0-foundation/documentation/blob/main/wrapped-M-audit-reports/Kirill%20Fedoseev%20Audit%20Report.md){rel=""nofollow""} | ## EVM M0-Extensions EVM M0-Extensions, smart contract extensions for the M0 platform, went through comprehensive audits in 2025. | Auditor | Date | Report | | ------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Certora | July 2025 | [Certora\_MExtension\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/Certora%20MExtension%20Security%20Assessment%20Final%20Report.pdf){rel=""nofollow""} | | ChainSecurity | July 2025 | [ChainSecurity\_MExtensions\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/ChainSecurity_M0_M_Extensions_audit_draft.pdf){rel=""nofollow""} | | Guardian | August 2025 | [Guardian\_MExtensions\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/Guardian%20Audits%20M0%20Extensions%20Report%20Aug%205.pdf){rel=""nofollow""} | ## Solana M0-Extensions Solana M0-Extensions, the Solana implementation of M0 platform stablecoin extensions, underwent thorough security audits in 2025. | Auditor | Date | Report | | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Halborn | June 2025 | [Halborn\_SolanaExtensions\_report.pdf](https://github.com/m0-foundation/solana-m-extensions/blob/main/audits/halborn_m_extensions_audit_report.pdf){rel=""nofollow""} | | Adevar | July 2025 | [Adevar\_SolanaExtensions\_report.pdf](https://github.com/m0-foundation/solana-m-extensions/blob/main/audits/adevar_m_extensions_audit_report.pdf){rel=""nofollow""} | | Ottersec | July 2025 | [Ottersec\_SolanaExtensions\_report.pdf](https://github.com/m0-foundation/solana-m-extensions/blob/main/audits/ottersec_m_extensions_audit_report.pdf){rel=""nofollow""} | ## M0 Portal Lite M0 Portal Lite, the lightweight cross-chain bridge solution, was audited by leading security firms in 2025. | Auditor | Date | Report | | --------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Three Sigma | April 2025 | [ThreeSigma\_PortalLite\_report.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/Three%20Sigma%20-%20M0PortalLite.pdf){rel=""nofollow""} | | ChainLight | May 2025 | [ChainLight\_PortalLite\_report.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/ChainLight%20-%20M%20Portal%20Lite%20Security%20Audit%20v1.0.pdf){rel=""nofollow""} | | ChainSecurity | September 12, 2025 | [ChainSecurity\_M0\_M\_Portal\_Lite\_audit.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/ChainSecurity_M0_M_Portal_Lite_audit.pdf){rel=""nofollow""} | | Guardian Audits | August 15, 2025 | [GuardianAudits\_M0\_PortalLite\_audit.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/GuardianAudits_M0_PortalLite_audit.pdf){rel=""nofollow""} | | Halborn | October 27, 2025 | [Halborn - M Portal Lite 10-27-25.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/Halborn%20-%20M%20Portal%20Lite%2010-27-25.pdf){rel=""nofollow""} | ## Solana M0 Solana M0, the core M0 platform implementation on Solana, received comprehensive security reviews in 2025. | Auditor | Date | Report | | -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Halborn | March 2025 | [Halborn\_SolanaM\_report.pdf](https://github.com/m0-foundation/solana-m/blob/main/audits/halborn_solana_m_audit.pdf){rel=""nofollow""} | | OtterSec | April 2025 | [OtterSec\_SolanaM\_report.pdf](https://github.com/m0-foundation/solana-m/blob/main/audits/ottersec_solana_m_audit.pdf){rel=""nofollow""} | | Sec3 | May 2025 | [Sec3\_SolanaM\_report.pdf](https://github.com/m0-foundation/solana-m/blob/main/audits/sec3_solana_m_audit_report.pdf){rel=""nofollow""} | ## mUSD mUSD, an M0 stablecoin extension designed for MetaMask, is an upgradeable ERC20 token contract derived from`MYieldToOne`, designed to wrap into a non-rebasing token, where all accrued rewards is claimable by a single designated recipient, underwent comprehensive security audits in August 2025. | Auditor | Date | Report | | ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ChainSecurity | August 26, 2025 | [ChainSecurity\_M0\_MUSD\_audit.pdf](https://github.com/m0-foundation/mUSD/blob/main/audits/ChainSecurity_M0_MUSD_audit.pdf){rel=""nofollow""} | | Consensys Diligence | August 2025 | [ConsensysDiligence\_M0\_MUSD\_audit.pdf](https://github.com/m0-foundation/mUSD/blob/main/audits/ConsensysDiligence_M0_MUSD_audit.pdf){rel=""nofollow""} | | Guardian Audits | August 15, 2025 | [GuardianAudits\_M0\_MUSD\_report.pdf](https://github.com/m0-foundation/mUSD/blob/main/audits/GuardianAudits_M0_MUSD_report.pdf){rel=""nofollow""} | | Kirill Fedoseev | August 6, 2025 | [MZero-review-report-v1-private.md](https://github.com/m0-foundation/mUSD/blob/main/audits/MZero-review-report-v1-private.md){rel=""nofollow""} | ## Limit Order Protocol The Limit Order Protocol is the core component of the M0 liquidity delivery system, enabling users to submit same-chain or cross-chain limit orders to exchange one token for another. The protocol underwent comprehensive security audits by leading firms. | Auditor | Date | Report | | --------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ChainSecurity | January 22, 2026 | [chainsecurity\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/v1/phase_one/chainsecurity_report.pdf){rel=""nofollow""} | | Guardian Audits | January 30, 2026 | [guardian\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/v1/phase_one/guardian_report.pdf){rel=""nofollow""} | | Halborn | January 14, 2026 | [halborn\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/v1/phase_one/halborn_report.pdf){rel=""nofollow""} | | Sherlock | December 16, 2025 | [sherlock\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/v1/phase_one/sherlock_report.pdf){rel=""nofollow""} | | Adevar | January 23, 2026 | [adevar\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/v1/phase_two/adevar_report.pdf){rel=""nofollow""} | | Guardian Audits | June 2026 | [guardian\_liquidity\_delivery\_update.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/v1.1/guardian_liquidity_delivery_update.pdf){rel=""nofollow""} | ## Multichain V2 Multichain V2, the next generation of M0's cross-chain infrastructure, underwent comprehensive security audits by leading firms. | Auditor | Date | Report | | --------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Adevar | January 21, 2026 | [Adevar\_M0\_Portal\_V2.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/Adevar_M0_Portal_V2.pdf){rel=""nofollow""} | | ChainSecurity | February 24, 2026 | [ChainSecurity\_M0\_Portal\_V2\_Liquidity\_Delivery.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/ChainSecurity_M0_Portal_V2_Liquidity_Delivery.pdf){rel=""nofollow""} | | Guardian Audits | January 30, 2026 | [Guardian\_M0\_Portal\_V2\_Liquidity\_Delivery.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/Guardian_M0_Portal_V2_Liquidity_Delivery.pdf){rel=""nofollow""} | | Halborn | January 16, 2026 | [Halborn\_M0\_Portal\_V2.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/Halborn_M0_Portal_V2.pdf){rel=""nofollow""} | | Sherlock | December 19, 2025 | [Sherlock\_M0\_Portal\_V2.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/Sherlock_M0_Portal_V2.pdf){rel=""nofollow""} | ## JMI (Just Mint It) JMI (Just Mint It), a multi-collateral extension for M tokens, underwent security audits in late 2025. | Auditor | Date | Report | | --------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Guardian Audits | December 12, 2025 | [M0\_EVM-M\_Extensions\_Review\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/JMI/M0_EVM-M_Extensions_Review_report.pdf){rel=""nofollow""} | | Sherlock | November 13, 2025 | [Sherlock\_JMI\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/JMI/2025_12_10_Final_M0_Collaborative_Audit_Report_1765332345.pdf){rel=""nofollow""} | ## PYUSDx PYUSDx is a developer platform built around PYUSD on M0 infrastructure. ChainSecurity, Guardian, and Sherlock audited its core contracts, and Halborn reviewed the Portal updates. | Auditor | Date | Report | | ------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | ChainSecurity | June 2026 | [ChainSecurity\_M0\_PYUSDX\_Audit\_Report.pdf](https://docs.m0.org/audits/pyusdx/ChainSecurity-M0_PYUSDX_Audit_Report.pdf) | | Guardian | June 2026 | [Guardian\_M0\_PYUSDX\_Audit\_Report.pdf](https://docs.m0.org/audits/pyusdx/Guardian-M0_PYUSDX_Audit_Report.pdf) | | Sherlock | June 2026 | [Sherlock\_M0\_PYUSDX\_Audit\_Report.pdf](https://docs.m0.org/audits/pyusdx/Sherlock-M0_PYUSDX_Audit_Report.pdf) | | Halborn | September 2026 | [Halborn\_M0\_PYUSDX\_Portal\_Updates\_Audit\_Report.pdf](https://docs.m0.org/audits/pyusdx/Halborn_M0_PYUSDX_Portal_Updates_Audit_Report.pdf) | # Glossary #### Earner A holder or distributor of M0-powered stablecoins whose address is approved to earn the Earner Rate. #### Earner Rate The annualized percentage paid in the Earn Mechanism. #### Eligible Collateral A description of portfolio composition which can be placed in Eligible Custody Solutions and be used to generate an onchain Collateral Value, which is subsequently used to mint M0-powered stablecoins. #### Eligible Custody Solution A description of entity structures, jurisdictions, contractual agreements, and other details that will suffice for the custody of Eligible Collateral. #### Mint Delay A period of time between when a Issuer has called Propose Mint and when they can first call Mint. It serves as a protective measure to ensure all actors have sufficient time to audit each Propose Mint. #### Mint Ratio The fraction size of a Issuer's onchain Collateral Value that they can mint. #### Issuer #### Issuer An institution that connects to the protocol to generate and manage stablecoin supply. #### Issuer Freeze Time A period of time during which an Issuer will be disabled to call Propose Mint and Mint methods. Can be called by any Validator on any Issuer by passing the Issuer address as the argument of the Freeze method. It can be called multiple times on the same Issuer to reset the Issuer Freeze Time window. If Freeze is called during an already existing Issuer Freeze Time, the Issuer Freeze Time window will restart from the beginning. #### Issuer Rate The annualized percentage charged continuously to Issuers on their outstanding debt. #### Issuer Burn The destruction of tokens that occurs when a Stellar asset is sent to its issuer address. Because this happens at the asset (SAC) layer outside the MGUSD Minter Gateway contract, the Minter Gateway's supply accumulators are not updated automatically; Admin calls `reconcile_burn` to bring bookkeeping back in line. See [MGUSD on Stellar](https://docs.m0.org/protocol/stellar). #### MGUSD An M0-powered Stellar Asset Contract (SAC) stablecoin, minted and burned via a Soroban Minter Gateway contract. See [MGUSD on Stellar](https://docs.m0.org/protocol/stellar). #### Penalty Rate The percentage charged for missing Update Collateral interval(s) or being undercollateralized on outstanding debt that is in excess of the amount a Issuers is permitted to have generated. It is assessed any time Accrue Penalty is called, which is embedded in both Update Collateral and Burn. This is a fixed percentage and not an annualized rate. #### Periphery Contract A smart contract that adds supplement functionality, but exists outside of the core M0 protocol. #### `POWER` One of the two utility tokens used in the M0 TTG. It is used to vote on active proposals and can be considered the primary management token of the mechanism. `POWER` holders will earn `ZERO` in exchange for their direct participation in governance. #### Propose Mint Time To Live The amount of time after the Mint Delay during which a Issuer can complete the mint operation before Propose Mint expires. It serves as a protective measure to ensure that Issuers cannot call Propose Mint and then execute the Mint at a much later date. #### SAC (Stellar Asset Contract) The native Stellar contract that holds token balances and handles standard SEP-41 transfers and account authorization. For MGUSD, the SAC is administered by the Soroban Minter Gateway contract, which is the sole authority for supply changes and compliance actions. #### Soroban Stellar's smart contract platform. The MGUSD Minter Gateway contract — which controls minting, burning, yield accrual, and compliance — is a Soroban contract. #### TTG TTG stands for Two Token Governor and is a mechanism by which holders of the voting tokens are penalized for failing to vote. There are two utility tokens used in the M0 TTG: `POWER` and `ZERO`. #### Update Collateral Interval This amount of time is the period between which Update Collateral must be called by a Issuer. If they do not call Update Collateral within this amount of time after their previous call, their onchain Collateral Value is set to 0 and they will incur Penalty Rate on the next update. #### Validator An independent entity that provides timely information about the offchain collateral being used to back M0-powered stablecoins. #### ZERO One of the two utility tokens used in the M0 TTG, is earned by `POWER` holders in exchange for their direct participation in governance. The `ZERO` holders are the recipient of all remaining value that is not paid out to the Earn Mechanism for their participation in protocol governance. The anticipated accumulation of tokens to the `ZERO` holders are Proposal Fee payments from proposal submission, the payments from `POWER` token auctions, and a portion of Issuer Rate and Penalty Rate charges to Issuers. # LLMs.txt M0 docs provide machine-readable text files that AI tools can consume to understand the M0 protocol, its terminology, and technical documentation. ## What is LLMs.txt? [LLMs.txt](https://llmstxt.org){rel=""nofollow""} is a standard format that provides website content in a way optimized for large language models. Instead of parsing HTML, AI tools can read structured markdown that contains the essential information. ## Available Routes | Route | Description | Size | | ------------------------- | ------------------------------------------------------- | -------------- | | `/llms.txt` | Overview of all documentation sections with page links | \~5K tokens | | `/llms-full.txt` | Complete documentation content — all pages concatenated | \~200K+ tokens | | `/llms/get-started.txt` | Get Started section only | Smaller | | `/llms/launch-guide.txt` | Launch Guide section only | Smaller | | `/llms/protocol-docs.txt` | Protocol Docs section only | Smaller | | `/llms/resources.txt` | Resources section only | Smaller | ## Choosing the Right File - Use **`/llms.txt`** when you need a quick overview or want to find the right section to dive into. - Use **section-specific files** (e.g., `/llms/protocol-docs.txt`) when you know which area you need and want to keep token usage low. - Use **`/llms-full.txt`** when you need comprehensive context across all M0 documentation. This is large — only use it with tools that support large context windows. ## Usage with AI Tools ### Cursor Add as a documentation source in your project's `.cursor/rules` file: ```text @https://docs.m0.org/llms-full.txt ``` Or reference a specific section: ```text @https://docs.m0.org/llms/protocol-docs.txt ``` ### Windsurf Add to your project rules or reference the URL directly in chat: ```text Refer to https://docs.m0.org/llms-full.txt for M0 documentation ``` ### ChatGPT, Claude, and Other Chat Tools Paste the URL directly in your conversation: ```text Read https://docs.m0.org/llms.txt and answer my questions about M0 ``` ::note For the best experience with M0 documentation, use the [MCP Server](https://docs.m0.org/agents/mcp-server) instead of LLMs.txt. MCP allows your AI assistant to search and fetch specific pages on demand rather than loading everything at once. :: # MCP Server M0 docs include a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io){rel=""nofollow""} server that lets AI assistants query, search, and navigate the documentation programmatically. ## What is MCP? MCP is an open protocol that standardizes how AI applications connect to external data sources and tools. By connecting your AI assistant to the M0 MCP server, it gains direct access to all M0 documentation — no copy-pasting required. ## Available Tools The M0 MCP server exposes the following tools: ### Documentation Tools | Tool | Description | | ------------- | ------------------------------------------------------------------------------------------------------------- | | `list-pages` | Lists all documentation pages with title, path, and description | | `get-page` | Retrieves the full markdown content of a specific page by path | | `search-docs` | Full-text search across all documentation pages | | `get-section` | Get all pages in a specific section (get-started, build, issuers, protocol, api-reference, resources, agents) | ### Reference Tools | Tool | Description | | --------------- | ------------------------------------------------------ | | `get-glossary` | Look up M0 protocol terminology definitions | | `get-addresses` | Look up contract addresses across all supported chains | ## Configuration Add the M0 MCP server to your AI assistant: ::tabs :::tabs-item{label="Claude Code"} ```bash claude mcp add --transport http m0-docs https://docs.m0.org/mcp ``` ::: :::tabs-item{label="Claude Desktop"} Add to your `claude_desktop_config.json`: ```json { "mcpServers": { "m0-docs": { "url": "https://docs.m0.org/mcp" } } } ``` ::: :::tabs-item{label="Cursor"} Add to your `.cursor/mcp.json`: ```json { "mcpServers": { "m0-docs": { "url": "https://docs.m0.org/mcp" } } } ``` ::: :::tabs-item{label="VS Code"} Add to your `.vscode/mcp.json`: ```json { "servers": { "m0-docs": { "type": "http", "url": "https://docs.m0.org/mcp" } } } ``` ::: :::tabs-item{label="Windsurf"} Add to your `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "m0-docs": { "serverUrl": "https://docs.m0.org/mcp" } } } ``` ::: :: ## Usage Examples Once connected, you can ask your AI assistant questions like: - "Search M0 docs for how minting works" - "Look up the M token address on Base" - "What does TTG mean in M0?" - "Show me all protocol documentation pages" - "Get the full content of the getting started guide" The assistant will use the appropriate MCP tools to fetch accurate, up-to-date information directly from the docs. # Skills Skills are pre-built instruction sets that give AI coding agents specialized knowledge about M0. They provide context about the protocol, smart contracts, integration patterns, and best practices — so you can build on M0 without needing to explain everything from scratch. ## What are Skills? Skills are markdown-based instruction files that AI coding tools load as context. They contain: - Protocol architecture and terminology - Smart contract interfaces and addresses - Integration patterns and code examples - Best practices and common pitfalls When installed, your AI assistant can help you write M0 integration code, debug contract interactions, and follow M0 conventions automatically. ## Available Skills | Skill | Description | Download | | ---------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | M0 Protocol | Core protocol concepts, M token, minting/burning, TTG governance | [m0-protocol.md](https://docs.m0.org/skills/m0-protocol.md){rel=""nofollow""} | | Stablecoin Integration | Building with Wrapped M, extension patterns, cross-chain bridging | [m0-stablecoin-integration.md](https://docs.m0.org/skills/m0-stablecoin-integration.md){rel=""nofollow""} | ## Installation ::tabs :::tabs-item{label="Claude Code"} Install skills by downloading them into your project's `.claude/skills/` directory: ```bash mkdir -p .claude/skills/m0-protocol curl -o .claude/skills/m0-protocol/SKILL.md https://docs.m0.org/skills/m0-protocol.md mkdir -p .claude/skills/m0-stablecoin-integration curl -o .claude/skills/m0-stablecoin-integration/SKILL.md https://docs.m0.org/skills/m0-stablecoin-integration.md ``` Claude Code will automatically pick up skills from `.claude/skills/` in your project directory. ::: :::tabs-item{label="Cursor"} Add M0 skills as context in your `.cursor/rules` file: ```text @https://docs.m0.org/skills/m0-protocol.md @https://docs.m0.org/skills/m0-stablecoin-integration.md ``` Cursor will use these files as context when you ask questions about M0. ::: :::tabs-item{label="Other AI Tools"} For tools that support custom instructions or system prompts, reference the skill files directly: - **Direct URL**: Add `https://docs.m0.org/skills/m0-protocol.md` as a context source - **LLMs.txt**: Use the [LLMs.txt](https://docs.m0.org/agents/llms-txt) files for broader documentation coverage - **MCP Server**: Connect via the [MCP Server](https://docs.m0.org/agents/mcp-server) for on-demand, searchable access ::: :: ## Usage Once installed, you can ask your AI assistant to: - "Create a contract that wraps M tokens" - "How do I integrate M0 minting into my dApp?" - "What's the correct way to call proposeMint()?" - "Set up cross-chain M token bridging" - "What are the steps to get earner approval for my extension?" The assistant will use its M0 knowledge to provide accurate, context-aware code and guidance.