Skip to content

Deploying a MEarnerManager Extension

This guide provides a step-by-step walkthrough for deploying a permissioned, institutional-grade MEarnerManager-based stablecoin. It assumes you have already chosen this model and understand its architecture.

If you haven't already, please review the MEarnerManager Deep Dive for conceptual details.

1. Setup Your Environment

First, you'll need to clone the m-extensions repository and set up your development environment:

  1. Clone the repository:
    git clone https://github.com/m0-foundation/m-extensions.git
    cd m-extensions
  2. Follow the setup instructions:
    Refer to the README.md and follow all the commands provided to install dependencies and configure your development environment.

  3. Configure Environment Variables: Create a .env file in the root directory:

    # Private key for deployment (include 0x prefix)
    PRIVATE_KEY=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
     
    # RPC URLs
    MAINNET_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/your-api-key
    SEPOLIA_RPC_URL=https://eth-sepolia.alchemyapi.io/v2/your-api-key
     
    # Etherscan API key for verification
    ETHERSCAN_API_KEY=your-etherscan-api-key

    Security Warning: Never commit your .env file to version control. Add it to your .gitignore:

    echo ".env" >> .gitignore

2. Create and Initialize Your Contract

Your custom contract will inherit from MEarnerManager. In the initialize function, you will set up the core roles and the initial fee recipient.

Create a new file in the src/ directory, for example, InstitutionalUSD.sol.

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
 
import { MEarnerManager } from "./projects/earnerManager/MEarnerManager.sol";
 
contract InstitutionalUSD is MEarnerManager {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor(address mToken_, address swapFacility_) MEarnerManager(mToken_, swapFacility_) {
        _disableInitializers();
    }
 
    function initialize(
        string memory name,
        string memory symbol,
        address admin,
        address earnerManager,
        address feeRecipient_
    ) public initializer {
        MEarnerManager.initialize(
            name,                    // "Institutional M USD"
            symbol,                  // "iMUSD"
            admin,                   // Admin multisig address
            earnerManager,           // Platform operator address
            feeRecipient_            // Protocol treasury address
        );
    }
}

Key Parameters Explained:

  • name: The full name of your token (e.g., "Institutional M USD")
  • symbol: The token symbol (e.g., "iMUSD")
  • admin: Address with DEFAULT_ADMIN_ROLE (should be a multisig)
  • earnerManager: Address with EARNER_MANAGER_ROLE (your platform operator)
  • feeRecipient_: Address that receives all protocol fees

Note: The mToken and swapFacility addresses are set in the constructor as immutable variables, not in the initializer.

3. Write Deployment Script

Create a deployment script in the script/ directory:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
 
import { Script } from "forge-std/Script.sol";
import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol";
import { InstitutionalUSD } from "../src/InstitutionalUSD.sol";
 
contract DeployInstitutionalUSD is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        address deployer = vm.addr(deployerPrivateKey);
 
        vm.startBroadcast(deployerPrivateKey);
        
        // Deploying a Transparent Proxy, which requires a proxy admin.
        address proxy = Upgrades.deployTransparentProxy(
            "InstitutionalUSD.sol",         // Contract file name
            deployer,                       // The admin for the proxy contract itself
            abi.encodeCall(                 // The initializer call data
                InstitutionalUSD.initialize,
                (
                    "Institutional M USD",    // name
                    "iMUSD",                  // symbol
                    0x...,                    // admin (DEFAULT_ADMIN_ROLE for the logic)
                    0x...,                    // earnerManager
                    0x...                     // feeRecipient
                )
            )
        );
 
        vm.stopBroadcast();
 
        console.log("InstitutionalUSD deployed at:", proxy);
    }
}

4. Testing Your Contract

Before deployment, create comprehensive tests that focus on the whitelisting logic and fee mechanisms:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
 
import { Test } from "forge-std/Test.sol";
import { InstitutionalUSD } from "../src/InstitutionalUSD.sol";
 
contract InstitutionalUSDTest is Test {
    InstitutionalUSD token;
    address admin = makeAddr("admin");
    address earnerManager = makeAddr("earnerManager");
    address feeRecipient = makeAddr("feeRecipient");
    address client1 = makeAddr("client1");
    address client2 = makeAddr("client2");
    address unauthorized = makeAddr("unauthorized");
 
    function setUp() public {
        token = new InstitutionalUSD(
            address(mockMToken),       // M Token address
            address(mockSwapFacility)  // SwapFacility address
        );
        
        token.initialize(
            "Institutional M USD",
            "iMUSD",
            admin,                     // Admin
            earnerManager,             // Earner manager
            feeRecipient               // Fee recipient
        );
    }
 
    function testWhitelisting() public {
        // Test that only EARNER_MANAGER_ROLE can whitelist
        vm.prank(earnerManager);
        token.setAccountInfo(client1, true, 500); // 5% fee rate
        
        assertTrue(token.isWhitelisted(client1));
        assertEq(token.feeRateOf(client1), 500);
    }
 
    function testUnauthorizedAccess() public {
        // Test that non-whitelisted addresses cannot interact
        vm.expectRevert(abi.encodeWithSelector(token.NotWhitelisted.selector, unauthorized));
        vm.prank(unauthorized);
        // This should fail since unauthorized is not whitelisted
        token.transfer(client1, 100);
    }
 
    function testYieldClaimWithFees() public {
        // Test yield claiming with different fee rates
        vm.prank(earnerManager);
        token.setAccountInfo(client1, true, 1000); // 10% fee
        
        // Mock yield accrual and test claiming
        // Verify correct fee split between client and feeRecipient
    }
 
    function testBatchWhitelisting() public {
        address[] memory accounts = new address[](2);
        bool[] memory statuses = new bool[](2);
        uint16[] memory feeRates = new uint16[](2);
        
        accounts[0] = client1;
        accounts[1] = client2;
        statuses[0] = true;
        statuses[1] = true;
        feeRates[0] = 500;  // 5%
        feeRates[1] = 1000; // 10%
        
        vm.prank(earnerManager);
        token.setAccountInfo(accounts, statuses, feeRates);
        
        assertTrue(token.isWhitelisted(client1));
        assertTrue(token.isWhitelisted(client2));
        assertEq(token.feeRateOf(client1), 500);
        assertEq(token.feeRateOf(client2), 1000);
    }
 
    function testEarningCanOnlyBeEnabledOnce() public {
        // Test that earning can only be enabled once
        token.enableEarning();
        
        vm.expectRevert(abi.encodeWithSelector(token.EarningCannotBeReenabled.selector));
        token.enableEarning();
    }
}

5. Gain M0 Earner Approval & Audit

Follow the standard procedure for getting your contract ready for mainnet:

  • Gain M0 Earner approval for your contract address via governance. See the Gaining Earner Approval guide.
  • Conduct a thorough security audit. Pay special attention to access control and the whitelisting logic.

6. Deploy and Enable Earning

Deploy to Testnet First

# Load environment variables
source .env
 
# Deploy to Sepolia
forge script script/DeployInstitutionalUSD.s.sol \
    --rpc-url $SEPOLIA_RPC_URL \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY \
    --broadcast

Deploy to Mainnet

# Load environment variables
source .env
 
# Deploy to Mainnet
forge script script/DeployInstitutionalUSD.s.sol \
    --rpc-url $MAINNET_RPC_URL \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY \
    --broadcast

Alternative: Hardware Wallet Deployment

For production deployments, consider using a hardware wallet:

forge script script/DeployInstitutionalUSD.s.sol \
    --rpc-url $MAINNET_RPC_URL \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY \
    --ledger \
    --broadcast

Enable Earning

Once governance approval is confirmed, call enableEarning() on your deployed contract to begin accruing yield:

// This starts the yield accrual process - can only be called ONCE
institutionalUSD.enableEarning();

Important: Remember that earning can only be enabled once. If you disable earning later, you cannot re-enable it.

7. Application Logic & Integration

This is the key step for MEarnerManager. Your application's backend or admin panel must be built to interact with the contract's permissioning system.

Backend Integration for Client Management

// Example Node.js/Web3 integration for client onboarding
 
class InstitutionalUSDManager {
    constructor(contract, earnerManagerSigner) {
        this.contract = contract;
        this.signer = earnerManagerSigner;
    }
 
    // Onboard a new institutional client
    async onboardClient(clientAddress, feeRateBps) {
        const tx = await this.contract
            .connect(this.signer)
            .setAccountInfo(clientAddress, true, feeRateBps);
        
        await tx.wait();
        console.log(`Client ${clientAddress} onboarded with ${feeRateBps} bps fee rate`);
    }
 
    // Batch onboard multiple clients
    async batchOnboardClients(clientData) {
        const addresses = clientData.map(c => c.address);
        const statuses = clientData.map(() => true);
        const feeRates = clientData.map(c => c.feeRate);
 
        const tx = await this.contract
            .connect(this.signer)
            .setAccountInfo(addresses, statuses, feeRates);
        
        await tx.wait();
        console.log(`Batch onboarded ${clientData.length} clients`);
    }
 
    // Offboard a client
    async offboardClient(clientAddress) {
        const tx = await this.contract
            .connect(this.signer)
            .setAccountInfo(clientAddress, false, 0);
        
        await tx.wait();
        console.log(`Client ${clientAddress} offboarded`);
    }
 
    // Update client fee rate
    async updateClientFeeRate(clientAddress, newFeeRate) {
        const tx = await this.contract
            .connect(this.signer)
            .setAccountInfo(clientAddress, true, newFeeRate);
        
        await tx.wait();
        console.log(`Updated ${clientAddress} fee rate to ${newFeeRate} bps`);
    }
 
    // Claim yield for a client
    async claimYieldForClient(clientAddress) {
        const tx = await this.contract.claimFor(clientAddress);
        const receipt = await tx.wait();
        
        // Parse events to get yield amounts
        const yieldEvent = receipt.events.find(e => e.event === 'YieldClaimed');
        return yieldEvent.args.yield;
    }
}

Client Management Workflows

  1. Onboarding Process:
    // When a new client is approved on your platform
    function onboardClient(address client, uint16 feeRate) external onlyAuthorized {
        // Verify client through your KYC/compliance process
        require(isKYCApproved(client), "Client not KYC approved");
        
        // Whitelist them on the contract
        institutionalUSD.setAccountInfo(client, true, feeRate);
        
        emit ClientOnboarded(client, feeRate);
    }
  2. Offboarding Process:
    // When a client relationship ends
    function offboardClient(address client) external onlyAuthorized {
        // Claim any pending yield first
        institutionalUSD.claimFor(client);
        
        // Remove from whitelist
        institutionalUSD.setAccountInfo(client, false, 0);
        
        emit ClientOffboarded(client);
    }
  3. Automated Yield Distribution:
    // Periodic yield claiming for all clients
    function distributeYieldToAllClients() external onlyAuthorized {
        address[] memory clients = getActiveClients();
        
        for (uint i = 0; i < clients.length; i++) {
            uint256 accruedYield = institutionalUSD.accruedYieldOf(clients[i]);
            if (accruedYield > minYieldThreshold) {
                institutionalUSD.claimFor(clients[i]);
            }
        }
    }

Frontend Integration for Clients

// Client dashboard showing their institutional account
 
// Check if user is whitelisted
const isWhitelisted = await institutionalUSD.isWhitelisted(clientAddress);
 
if (isWhitelisted) {
    // Get client's fee rate
    const feeRate = await institutionalUSD.feeRateOf(clientAddress);
    
    // Get current balance
    const balance = await institutionalUSD.balanceOf(clientAddress);
    
    // Get balance including unclaimed yield
    const balanceWithYield = await institutionalUSD.balanceWithYieldOf(clientAddress);
    
    // Get detailed yield information
    const [yieldWithFee, fee, yieldNetOfFee] = await institutionalUSD.accruedYieldAndFeeOf(clientAddress);
    
    // Get principal amount
    const principal = await institutionalUSD.principalOf(clientAddress);
    
    // Display to client
    console.log(`Balance: ${balance}`);
    console.log(`Total with yield: ${balanceWithYield}`);
    console.log(`Unclaimed yield (net): ${yieldNetOfFee}`);
    console.log(`Fee rate: ${feeRate / 100}%`);
    console.log(`Principal: ${principal}`);
}

8. Monitoring & Maintenance

Key Metrics to Track

  • Active Clients: Number of whitelisted addresses
  • Fee Revenue: Total fees collected by the protocol
  • Client Yield: Individual and aggregate yield distributed to clients
  • Average Fee Rate: Weighted average fee rate across all clients
  • Total Principal: Total principal amount across all clients

Operational Considerations

  • Regular Yield Claims: Set up automated processes to claim yield for clients regularly
  • Fee Rate Optimization: Monitor and adjust individual client fee rates based on business relationships
  • Compliance Monitoring: Track all whitelist changes for audit trails
  • Gas Optimization: Batch operations when possible to reduce transaction costs

Administrative Functions

// Example admin functions for ongoing management
 
// Update the global fee recipient
await institutionalUSD.setFeeRecipient(newTreasuryAddress);
 
// Get contract state information
const feeRecipient = await institutionalUSD.feeRecipient();
const totalPrincipal = await institutionalUSD.totalPrincipal();
const projectedSupply = await institutionalUSD.projectedTotalSupply();
const isEarningEnabled = await institutionalUSD.isEarningEnabled();
const wasEarningEnabled = await institutionalUSD.wasEarningEnabled();
const disableIndex = await institutionalUSD.disableIndex();
const currentIndex = await institutionalUSD.currentIndex();
 
// Batch claim for multiple clients
const clients = [client1, client2, client3];
const [yieldWithFees, fees, yieldNetOfFees] = await institutionalUSD.claimFor(clients);

Error Handling

Be prepared to handle these specific errors:

// Handle MEarnerManager specific errors
try {
    await institutionalUSD.setAccountInfo(client, true, feeRate);
} catch (error) {
    if (error.message.includes("NotWhitelisted")) {
        console.log("Account not whitelisted for this operation");
    } else if (error.message.includes("InvalidFeeRate")) {
        console.log("Fee rate exceeds maximum allowed (10,000 bps)");
    } else if (error.message.includes("InvalidAccountInfo")) {
        console.log("Cannot set non-zero fee rate for non-whitelisted account");
    }
}

Congratulations! You now have a fully functional institutional-grade stablecoin that provides:

  • Granular Control: Individual fee rates and whitelisting for each client
  • Compliance Ready: Built-in permissioning system for regulatory requirements
  • Revenue Generation: Customizable fee structures for different client tiers
  • Professional Grade: Enterprise-level access control and audit trails
  • One-Time Earning: Secure earning mechanism that can only be enabled once