Deep Dive: MEarnerManager Extension
The MEarnerManager
extension is a powerful model designed for institutional, B2B, and permissioned use cases. It provides granular, per-account control over who can hold the token and what share of the yield they receive.
This model is the perfect choice for fintech platforms, prime brokerages, or any application serving a limited number of clients who require bespoke, on-chain enforced fee structures. Its core feature is a whitelist: only approved addresses can interact with the token.
Source Code: MEarnerManager.sol
Architecture and Mechanism
The architecture of MEarnerManager
revolves around a central manager role that curates a list of approved "earners" with individual fee arrangements.
Roles (Access Control)
DEFAULT_ADMIN_ROLE
: The contract administrator, responsible for granting and revoking roles.EARNER_MANAGER_ROLE
: The gatekeeper and controller of the extension's economics. This role is responsible for:- Whitelisting and de-listing accounts via
setAccountInfo
- Setting a custom
feeRate
for each whitelisted account (0-10,000 basis points) - Setting the global
feeRecipient
address that collects all fees
- Whitelisting and de-listing accounts via
How It Works
- Whitelisting is Mandatory: Before any address can hold, receive, wrap, unwrap, approve, or transfer the token, it must be added to the whitelist by the
EARNER_MANAGER_ROLE
via thesetAccountInfo
function. - Custom Fee Rates: When an account is whitelisted, the manager assigns it a specific
feeRate
(from 0% to 100%). This allows for different commercial agreements with different clients. - Earning Control: The contract can only enable earning once using
enableEarning()
. Once disabled, it cannot be re-enabled (enforced by theEarningCannotBeReenabled
error). - Yield Accrual: The contract uses a principal-based system similar to
MYieldFee
. Each user has aprincipal
amount and their yield is calculated based on the difference between their present amount and current principal balance. - On-Demand Claiming: Yield for a specific user is only realized when
claimFor(account)
is called. This can be called by anyone on behalf of the whitelisted account. - Fee Splitting: The
claimFor
function calculates the total yield for the account since its last claim, then splits it:- The user receives
Total Yield * (1 - feeRate)
- The
feeRecipient
receivesTotal Yield * feeRate
- The user receives
This mechanism provides ultimate control and ensures that all token interactions are confined to a known set of participants.
Key Interface & Functions
Storage Layout
The storage is designed around the Account
struct, keeping each user's data neatly organized.
// Each whitelisted user has an Account struct
struct Account {
uint256 balance; // Current token balance
bool isWhitelisted; // Whitelist status
uint16 feeRate; // Fee rate in basis points (0-10,000)
uint112 principal; // Principal amount for yield calculation
}
struct MEarnerManagerStorageStruct {
address feeRecipient;
uint256 totalSupply;
uint112 totalPrincipal;
bool wasEarningEnabled;
uint128 disableIndex;
mapping(address account => Account) accounts;
}
Management Functions (The Control Panel)
These are the primary functions used by the EARNER_MANAGER_ROLE
.
setAccountInfo(address account, bool status, uint16 feeRate) external
: The main function to manage the whitelist. It can add an account, remove it, or update itsfeeRate
. Claims yield for the account before making changes. Reverts withInvalidAccountInfo
if trying to set a non-zero fee rate for a non-whitelisted account.setAccountInfo(address[] calldata accounts, bool[] calldata statuses, uint16[] calldata feeRates) external
: A batch version for managing multiple accounts in one transaction. Reverts withArrayLengthMismatch
if array lengths don't match orArrayLengthZero
if arrays are empty.setFeeRecipient(address feeRecipient_) external
: Sets or updates the single address that will receive all collected fees. The fee recipient is automatically whitelisted with a 0% fee rate.
Core Yield & Claim Functions
claimFor(address account) external
: The function to trigger a yield claim and fee split for a specific whitelisted account. Returns(yieldWithFee, fee, yieldNetOfFee)
. Can be called by anyone.claimFor(address[] calldata accounts) external
: Batch version that claims yield for multiple accounts. Returns arrays of yield amounts, fees, and net yields.
Earning Control Functions
enableEarning() external
: Starts earning yield on the held$M
. Can only be called once - if earning was previously enabled, it reverts withEarningCannotBeReenabled
.disableEarning() external
: Stops earning yield and records the disable index. Reverts withEarningIsDisabled
if already disabled.
View Functions
isWhitelisted(address account) external view
: A simple check to see if an account is on the approved list. This is used as a gate for all transfers, wraps, and unwraps.feeRateOf(address account) external view
: Returns the custom fee rate for a specific account.principalOf(address account) external view
: Returns the principal amount for an account.accruedYieldAndFeeOf(address account) external view
: Calculates the current claimable yield, the fee portion, and the net yield for an account without executing the claim. Returns(yieldWithFee, fee, yieldNetOfFee)
.accruedYieldOf(address account) external view
: Returns only the net yield after fees for an account.accruedFeeOf(address account) external view
: Returns only the fee portion of the yield for an account.balanceWithYieldOf(address account) external view
: Returns the account's current balance plus accrued net yield.feeRecipient() external view
: Returns the current fee recipient address.totalPrincipal() external view
: Returns the total principal of all accounts.projectedTotalSupply() external view
: Shows what the total supply would be if all yield was claimed.disableIndex() external view
: Returns the index when earning was disabled (0 if never disabled).wasEarningEnabled() external view
: Returns whether earning was ever enabled.isEarningEnabled() external view
: Returns whether earning is currently enabled (was enabled and not disabled).currentIndex() external view
: Returns the current index for yield calculations. Uses the disable index if earning is disabled, otherwise uses the M token's current index.
Access Control Enforcement
All operations (wrap, unwrap, transfer, approve) enforce that all involved parties are whitelisted:
_beforeApprove()
- Checks both account and spender are whitelisted_beforeWrap()
- Checks earning is enabled and both depositor and recipient are whitelisted_beforeUnwrap()
- Checks the account is whitelisted_beforeTransfer()
- Checks msg.sender, sender, and recipient are all whitelisted
Constants
ONE_HUNDRED_PERCENT
: Set to 10,000 (representing 100% in basis points)EARNER_MANAGER_ROLE
:keccak256("EARNER_MANAGER_ROLE")
Error Handling
The contract includes comprehensive error handling:
NotWhitelisted(address account)
: Thrown when a non-whitelisted account tries to interactEarningCannotBeReenabled
: Thrown when trying to enable earning after it was already enabled onceInvalidFeeRate
: Thrown when fee rate exceeds 100%InvalidAccountInfo
: Thrown when trying to set non-zero fee rate for non-whitelisted accountZeroAdmin
: Thrown when admin address is 0x0ZeroEarnerManager
: Thrown when earner manager address is 0x0ZeroFeeRecipient
: Thrown when fee recipient address is 0x0ZeroAccount
: Thrown when account address is 0x0ArrayLengthMismatch
: Thrown when array lengths don't match in batch operationsArrayLengthZero
: Thrown when arrays are empty in batch operations
Ready to build? Follow the implementation guide to deploy your MEarnerManager extension.