Skip to content

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

How It Works

  1. 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 the setAccountInfo function.
  2. 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.
  3. Earning Control: The contract can only enable earning once using enableEarning(). Once disabled, it cannot be re-enabled (enforced by the EarningCannotBeReenabled error).
  4. Yield Accrual: The contract uses a principal-based system similar to MYieldFee. Each user has a principal amount and their yield is calculated based on the difference between their present amount and current principal balance.
  5. 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.
  6. 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 receives Total Yield * feeRate

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 its feeRate. Claims yield for the account before making changes. Reverts with InvalidAccountInfo 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 with ArrayLengthMismatch if array lengths don't match or ArrayLengthZero 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 with EarningCannotBeReenabled.
  • disableEarning() external: Stops earning yield and records the disable index. Reverts with EarningIsDisabled 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 interact
  • EarningCannotBeReenabled: Thrown when trying to enable earning after it was already enabled once
  • InvalidFeeRate: Thrown when fee rate exceeds 100%
  • InvalidAccountInfo: Thrown when trying to set non-zero fee rate for non-whitelisted account
  • ZeroAdmin: Thrown when admin address is 0x0
  • ZeroEarnerManager: Thrown when earner manager address is 0x0
  • ZeroFeeRecipient: Thrown when fee recipient address is 0x0
  • ZeroAccount: Thrown when account address is 0x0
  • ArrayLengthMismatch: Thrown when array lengths don't match in batch operations
  • ArrayLengthZero: Thrown when arrays are empty in batch operations

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