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 thefeeRecipient
address.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 tokenbalance
is recorded, along with aprincipal
value. Think ofprincipal
as their "share" of the total pool. - The Growing Index: The contract maintains a
currentIndex
which starts at1e12
(EXP_SCALED_ONE) and grows over time based on the$M
yield 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)
, theirbalance
is 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$M
balance 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):
MYieldFee
usesblock.timestamp
to 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,MSpokeYieldFee
uses aRateOracle
. The index growth is capped by thelatestUpdateTimestamp
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. EmitsYieldClaimed
andTransfer
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 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$M
rate minus the protocol'sfeeRate
:(ONE_HUNDRED_PERCENT - feeRate) * _currentEarnerRate() / ONE_HUNDRED_PERCENT
.updateIndex() external
: Function that updates the index by refreshing thelatestIndex
,latestRate
, andlatestUpdateTimestamp
in storage. This is called automatically during key operations and returns the updated index.
Management Functions
setFeeRate(uint16 feeRate_) external
: Callable by theFEE_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 theFEE_MANAGER_ROLE
to change the treasury address. Claims any existing fees for the previous recipient first.setClaimRecipient(address account, address claimRecipient) external
: Callable by theCLAIM_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 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.