User Yield Model
Core Concept: Yield is automatically distributed to all token holders, creating a yield-bearing stablecoin. An optional protocol fee can be configured to capture a percentage for the admin.
When to Use:- DeFi protocols offering yield-bearing stablecoins to users
- Consumer wallets with built-in savings accounts
- GameFi economies where users are rewarded for holding
- Applications prioritizing user incentives while optionally capturing protocol revenue
EVM Implementation: MYieldFee
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.
SVM Implementation: ScaledUi
The ScaledUi extension is the most seamless way to create a yield-bearing stablecoin for end-users on Solana. It uses the scaled-ui mechanism to automatically and efficiently distribute yield to all token holders.
This model is perfect for DeFi protocols, consumer wallets, and any application that wants to offer a compelling stablecoin where user balances grow automatically in their wallets without requiring any transactions or claims.
Source Code: The ScaledUi model is compiled from the m_ext program using the scaled-ui Rust feature flag. The source code is available in the solana-m-extensions repository.
Architecture and Mechanism
The ScaledUi model combines the yield-generating power of $M with the scaled-ui mechanism to create a truly native, yield-bearing asset. The scaled-ui variant can have an optional fee that goes to the admin.
Roles (Access Control)
admin: The root administrator, responsible for initializing the contract and managingwrap_authorities.wrap_authority: An address authorized by theadminto perform wrap and unwrap operations, typically a trusted swap program.- Keeper/Anyone: The core
syncfunction is permissionless, allowing any user or automated keeper bot to call it to keep the yield index up to date.
How It Works: Onchain Indexing & Scaled-UI
- Initialization: The extension is deployed with the scaled-ui mechanism enabled.
- Wrapping: Users wrap
$Mvia an authorizedwrap_authorityand receive the extension token. The underlying$Mis held in the contract's vault (m_vault). Extensions automatically sync on wrap/unwrap. - Yield Accrual: The
m_vaultis registered as an earner in the base$Mprogram, and its$Mbalance begins to grow as yield accrues. - Permissionless Sync: Anyone can call the
sync()instruction, or bridge an index update. This is the engine of the yield distribution. - Index Calculation: The
sync()function reads the current index from the base$Mearnprogram and compares it to the last synced index stored in the extension'sglobal_account. It calculates the yield accrued in the vault during that period. - Mint Update: Using this new yield data, the program calculates the new effective scaled-ui rate for the extension token.
- Automatic Balance Updates: Wallets and explorers will display a higher balance for all holders, reflecting the accrued yield based on the scaled-ui mechanism. The user's "principal" balance (
amount) doesn't change, but their "UI amount" grows with each sync. - Optional Fee: If configured, a fee can be charged that goes to the admin.
This mechanism is incredibly efficient, as a single sync transaction updates the yield for every token holder simultaneously.
Key Interface & Functions
The model's logic is powered by the generic M Extension program, configured to operate in a rebasing mode.
Storage Layout
The ExtGlobal account stores the configuration and state, with YieldConfig being crucial for tracking the index synchronization.
// State structure from m_ext program
pub struct ExtGlobal {
pub admin: Pubkey,
pub ext_mint: Pubkey, // The extension mint using scaled-ui
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<Pubkey>,
}
pub struct YieldConfig {
pub variant: YieldVariant, // Set to YieldVariant::ScaledUi
pub last_m_index: u64, // Tracks the last synced index from the base M program
pub last_ext_index: u64, // Tracks the extension's own index
}Core Yield Function
sync(): A permissionless instruction that updates the yield index. It reads the state of the base$Mearnprogram and the extension'sm_vaultbalance. It then calculates the new scaled-ui rate. Theext_mintaccount is writable in this instruction, which is essential for this mechanism to work. Index updates can also be bridged, and extensions automatically sync on wrap/unwrap operations.
Management Functions
initialize(wrap_authorities: Vec<Pubkey>): Sets up the extension's global state with the ScaledUi 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.set_fee(fee_bps: u64): Set optional fee in basis points (goes to admin, typically 0 for full pass-through).
Standard M Extension Functions
wrap(amount: u64): Wraps$Minto the extension token. Callable by a whitelistedwrap_authority.unwrap(amount: u64): Unwraps the extension token back into$M. The amount specified is the principal amount; the user receives the principal plus any accrued yield. Callable by a whitelistedwrap_authority.
Error Handling
Unauthorized: Thrown if a non-authorized address attempts a privileged action.InvalidMint: Ensures the correct mints are being used inwrap/unwrap.MathOverflow/MathUnderflow: Protects against errors during the complex index calculations in thesyncfunction.
Ready to build? Follow the implementation guide to deploy your ScaledUi extension.

