Skip to content

Deep Dive: MYieldToOne Extension

The MYieldToOne extension is the simplest and most direct way to build on M0. It creates an upgradeable ERC-20 token that wraps $M on a 1:1 basis, while directing 100% of the accrued $M yield to a single, designated recipient address.

This model is perfect for builders who want the stability and liquidity of an $M-backed token within their ecosystem, but prefer to centralize the yield for strategic purposes like funding protocol development, ecosystem grants, or operational expenses.

Source Code: MYieldToOne.sol

Architecture and Mechanism

The beauty of MYieldToOne lies in its simplicity. It separates the token holding experience from the yield 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 yield goes. 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 $M through the SwapFacility and receives an equal amount of your new extension token (e.g., wrap 100 $M, receive 100 YourToken). The MYieldToOne contract now holds 100 $M.
  2. Accruing Yield: Your deployed MYieldToOne contract must call enableEarning() to start earning $M. The 100 $M it holds begins to accrue yield from the underlying US Treasury bills.
  3. Creating a Surplus: As yield accrues, the contract's $M balance becomes greater than the total supply of your extension token. For example, the contract might hold 100.05 $M while the totalSupply() of YourToken is still 100.
  4. Claiming Yield: This surplus is the claimable yield. Anyone can call the claimYield() function to realize this yield.
  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 yield is 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.

struct MYieldToOneStorageStruct {
    uint256 totalSupply;
    address yieldRecipient;
    mapping(address account => uint256 balance) balanceOf;
}

Core Yield Functions

  • yield() external view returns (uint256)
    This view function calculates the amount of claimable yield at any moment. It returns the difference between the contract's current $M balance and the totalSupply of the extension token: mBalance > totalSupply ? mBalance - totalSupply : 0.

  • claimYield() external returns (uint256)
    This is the function that triggers the yield distribution. It calculates the yield, mints that amount of new tokens, and transfers them to the yieldRecipient. Returns 0 if there is no yield to claim.

Management Functions

  • setYieldRecipient(address yieldRecipient) external
    Callable only by the YIELD_RECIPIENT_MANAGER_ROLE, this function updates the address that receives the yield. It automatically claims any existing yield 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 yield on held $M
  • disableEarning() - Stops earning yield

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? Follow the implementation guide to deploy your MYieldToOne extension.