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.
- L1 (Ethereum):
MYieldFee.sol - L2 (Spoke Chains):
MSpokeYieldFee.sol
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 thefeeRate(0-10,000 bps) and thefeeRecipientaddress.CLAIM_RECIPIENT_MANAGER_ROLE: Can redirect a user's yield claims to another address viasetClaimRecipient(). Useful for features like auto-compounding vaults or delegating rewards.
How It Works: Continuous Indexing
- Principal vs. Balance: When a user wraps
$M, their tokenbalanceis recorded, along with aprincipalvalue. Think ofprincipalas their "share" of the total pool. - The Growing Index: The contract maintains a
currentIndexwhich starts at1e12(EXP_SCALED_ONE) and grows over time based on the$Myield rate (minus the protocol fee). - 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. TheaccruedYieldOf(account)is the difference between this true balance and their last claimedbalance. - Claiming: When a user calls
claimYieldFor(account), theirbalanceis updated to the current true value, effectively "cashing in" their accrued yield. - The Fee: The protocol fee is what's left over. The
totalAccruedFee()is calculated as the contract's total$Mbalance minus the projected total supply owed to all users. This surplus can be claimed by anyone viaclaimFee(), which mints new tokens to thefeeRecipient.
L1 vs. L2 (MSpokeYieldFee)
The mechanism has a slight variation for L2 deployments.
- On L1 (Ethereum):
MYieldFeeusesblock.timestampto calculate thecurrentIndex, as yield accrues continuously. - On L2s (
MSpokeYieldFee): To prevent over-printing tokens due to the delay in$M's index propagation from L1,MSpokeYieldFeeuses aRateOracle. The index growth is capped by thelatestUpdateTimestampfrom the bridged$Mtoken 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. EmitsYieldClaimedandTransferevents.totalAccruedFee() external view: Shows the total fee amount available for the protocol to claim. Calculated as the contract's$Mbalance minus the projected total supply.claimFee() external: Mints the accrued fee amount and sends it to thefeeRecipient. 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$Mrate minus the protocol'sfeeRate:(ONE_HUNDRED_PERCENT - feeRate) * _currentEarnerRate() / ONE_HUNDRED_PERCENT.updateIndex() external: Function that updates the index by refreshing thelatestIndex,latestRate, andlatestUpdateTimestampin storage. This is called automatically during key operations and returns the updated index.
Management Functions
setFeeRate(uint16 feeRate_) external: Callable by theFEE_MANAGER_ROLEto 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 theFEE_MANAGER_ROLEto change the treasury address. Claims any existing fees for the previous recipient first.setClaimRecipient(address account, address claimRecipient) external: Callable by theCLAIM_RECIPIENT_MANAGER_ROLEto 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 callsIMTokenLike(mToken).startEarning(). Reverts if earning is already enabled.disableEarning() external: Stops earning yield. Updates the index, resets earning state, and callsIMTokenLike(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.

