Skip to content

Deep Dive: MYieldFee Extension

The MYieldFee extension is the ideal model for creating a user-facing, yield-bearing stablecoin. It allows you to build a custom token where yield continuously accrues to all holders, while giving you, the protocol developer, the ability to take a small, configurable fee on that yield.

This model is perfect for dApps, wallets, and DeFi protocols that want to offer a compelling, native stablecoin that shares the underlying $M yield with its users, creating a powerful incentive for adoption and liquidity.

Source Code:

Architecture and Mechanism

MYieldFee uses a sophisticated continuous indexing mechanism to distribute yield fairly and efficiently to all token holders.

Roles (Access Control)

MYieldFee defines three key management roles:

  • DEFAULT_ADMIN_ROLE: The root administrator, capable of granting and revoking other roles.
  • FEE_MANAGER_ROLE: Controls the protocol's revenue. This role can set the feeRate (0-10,000 bps) and the feeRecipient address.
  • CLAIM_RECIPIENT_MANAGER_ROLE: Can redirect a user's yield claims to another address via setClaimRecipient(). Useful for features like auto-compounding vaults or delegating rewards.

How It Works: Continuous Indexing

  1. Principal vs. Balance: When a user wraps $M, their token balance is recorded, along with a principal value. Think of principal as their "share" of the total pool.
  2. The Growing Index: The contract maintains a currentIndex which starts at 1e12 (EXP_SCALED_ONE) and grows over time based on the $M yield rate (minus the protocol fee).
  3. Accruing Yield: A user's "true" balance at any time is calculated using IndexingMath.getPresentAmountRoundedDown(principal, currentIndex). Since the index is always increasing, their effective balance grows continuously. The accruedYieldOf(account) is the difference between this true balance and their last claimed balance.
  4. Claiming: When a user calls claimYieldFor(account), their balance is updated to the current true value, effectively "cashing in" their accrued yield.
  5. The Fee: The protocol fee is what's left over. The totalAccruedFee() is calculated as the contract's total $M balance minus the projected total supply owed to all users. This surplus can be claimed by anyone via claimFee(), which mints new tokens to the feeRecipient.

L1 vs. L2 (MSpokeYieldFee)

The mechanism has a slight variation for L2 deployments.

  • On L1 (Ethereum): MYieldFee uses block.timestamp to calculate the currentIndex, as yield accrues continuously.
  • On L2s (MSpokeYieldFee): To prevent over-printing tokens due to the delay in $M's index propagation from L1, MSpokeYieldFee uses a RateOracle. The index growth is capped by the latestUpdateTimestamp from the bridged $M token on the L2, ensuring its growth never outpaces the underlying asset.

Key Interface & Functions

Storage Layout

The state management is more complex to handle the indexing logic.

struct MYieldFeeStorageStruct {
    uint256 totalSupply;
    uint112 totalPrincipal;
    uint128 latestIndex;
    uint16 feeRate;
    address feeRecipient;
    uint40 latestUpdateTimestamp;
    uint32 latestRate;
    bool isEarningEnabled;
    mapping(address account => uint256 balance) balanceOf;
    mapping(address account => uint112 principal) principalOf;
    mapping(address account => address claimRecipient) claimRecipients;
}

Core Yield Functions

  • accruedYieldOf(address account) external view: Returns the amount of unclaimed yield for a specific user. Calculated as the difference between their present amount (based on principal and current index) and their recorded balance.
  • claimYieldFor(address account) external: Claims the accrued yield for a user, updating their balance. Can be called by anyone on behalf of the user. If a claim recipient is set, the yield is automatically transferred to that address. Emits YieldClaimed and Transfer events.
  • totalAccruedFee() external view: Shows the total fee amount available for the protocol to claim. Calculated as the contract's $M balance minus the projected total supply.
  • claimFee() external: Mints the accrued fee amount and sends it to the feeRecipient. Returns the amount claimed. Can be called by anyone.

Indexing Functions

These functions are the heart of the yield distribution mechanism.

  • currentIndex() external view: Calculates the current, up-to-the-second index value using continuous indexing math. This is the core of the yield calculation.
  • earnerRate() external view: Returns the effective yield rate for token holders, which is the underlying $M rate minus the protocol's feeRate: (ONE_HUNDRED_PERCENT - feeRate) * _currentEarnerRate() / ONE_HUNDRED_PERCENT.
  • updateIndex() external: Function that updates the index by refreshing the latestIndex, latestRate, and latestUpdateTimestamp in storage. This is called automatically during key operations and returns the updated index.

Management Functions

  • setFeeRate(uint16 feeRate_) external: Callable by the FEE_MANAGER_ROLE to adjust the protocol's take rate (in basis points, 0-10,000). Updates the index if earning is enabled.
  • setFeeRecipient(address feeRecipient_) external: Callable by the FEE_MANAGER_ROLE to change the treasury address. Claims any existing fees for the previous recipient first.
  • setClaimRecipient(address account, address claimRecipient) external: Callable by the CLAIM_RECIPIENT_MANAGER_ROLE to redirect yield claims. Claims any existing yield for the account before making the change.

Earning Control Functions

  • enableEarning() external: Starts earning yield on the held $M. Updates the index and calls IMTokenLike(mToken).startEarning(). Reverts if earning is already enabled.
  • disableEarning() external: Stops earning yield. Updates the index, resets earning state, and calls IMTokenLike(mToken).stopEarning(address(this)). Reverts if earning is already disabled.

View Functions

  • balanceWithYieldOf(address account) external view: Returns the user's current balance plus any unclaimed yield.
  • principalOf(address account) external view: Returns the user's principal amount.
  • projectedTotalSupply() external view: Shows what the total supply would be if all yield was claimed.
  • totalAccruedYield() external view: Shows the total unclaimed yield for all users.
  • totalPrincipal() external view: Returns the total principal of all users.
  • claimRecipientFor(address account) external view: Returns the claim recipient for an account (defaults to the account itself if none set).
  • feeRate() external view: Returns the current fee rate in basis points.
  • feeRecipient() external view: Returns the current fee recipient address.
  • isEarningEnabled() external view: Returns whether earning is currently enabled.
  • latestIndex() external view: Returns the last stored index.
  • latestRate() external view: Returns the last stored rate.
  • latestUpdateTimestamp() external view: Returns the timestamp of the last index update.

Constants

  • ONE_HUNDRED_PERCENT: Set to 10,000 (representing 100% in basis points)
  • FEE_MANAGER_ROLE: keccak256("FEE_MANAGER_ROLE")
  • CLAIM_RECIPIENT_MANAGER_ROLE: keccak256("CLAIM_RECIPIENT_MANAGER_ROLE")

MSpokeYieldFee Specific

For L2 deployments, MSpokeYieldFee adds:

  • rateOracle() external view: Returns the address of the rate oracle used to get the earner rate.

Ready to build? Follow the implementation guide to deploy your MYieldFee extension.