Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Implementing the JMI Model

This guide provides step-by-step instructions for deploying a JMI (Just Mint It) extension, where multiple collateral types are accepted and 100% of the yield is captured by a designated recipient.

If you haven't already, please review the JMI Deep Dive for conceptual details about this model.

EVM Implementation

This section covers deploying the JMIExtension contract on EVM chains (Ethereum, Base, Arbitrum, etc.).

1. Setup Your Environment

First, 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 commands 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:

    echo ".env" >> .gitignore

2. Create Your Contract

Your custom JMI extension will inherit from JMIExtension. Create a new file in the src/ directory, for example, MyJMIToken.sol:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
 
import { JMIExtension } from "./projects/jmi/JMIExtension.sol";
 
contract MyJMIToken is JMIExtension {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor(address mToken_, address swapFacility_) JMIExtension(mToken_, swapFacility_) {
        _disableInitializers();
    }
 
    function initialize(
        string memory name,
        string memory symbol,
        address yieldRecipient_,
        address admin,
        address assetCapManager,
        address freezeManager,
        address pauser,
        address yieldRecipientManager
    ) public override initializer {
        JMIExtension.initialize(
            name,                    // "My JMI USD"
            symbol,                  // "jUSD"
            yieldRecipient_,         // Treasury wallet address
            admin,                   // Admin multisig address
            assetCapManager,         // Asset cap manager address
            freezeManager,           // Freeze manager address
            pauser,                  // Pauser address
            yieldRecipientManager    // Yield recipient manager address
        );
    }
}

Key Parameters Explained

  • name: The full name of your token (e.g., "My JMI USD")
  • symbol: The token symbol (e.g., "jUSD")
  • yieldRecipient_: The wallet receiving all yield (your treasury)
  • admin: Address with DEFAULT_ADMIN_ROLE (should be a multisig)
  • assetCapManager: Address with ASSET_CAP_MANAGER_ROLE (manages collateral caps)
  • freezeManager: Address with FREEZE_MANAGER_ROLE (can freeze addresses)
  • pauser: Address with PAUSER_ROLE (emergency stop)
  • yieldRecipientManager: Address with YIELD_RECIPIENT_MANAGER_ROLE

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

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, console } from "forge-std/Script.sol";
import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol";
import { MyJMIToken } from "../src/MyJMIToken.sol";
 
contract DeployMyJMIToken is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        address deployer = vm.addr(deployerPrivateKey);
 
        // Configuration - update these addresses for your deployment
        address mToken = 0x...; // M Token address for your chain
        address swapFacility = 0x...; // SwapFacility address for your chain
 
        vm.startBroadcast(deployerPrivateKey);
 
        address proxy = Upgrades.deployTransparentProxy(
            "MyJMIToken.sol",
            deployer,
            abi.encodeCall(
                MyJMIToken.initialize,
                (
                    "My JMI USD",           // name
                    "jUSD",                 // symbol
                    0x...,                  // yieldRecipient
                    0x...,                  // admin
                    0x...,                  // assetCapManager
                    0x...,                  // freezeManager
                    0x...,                  // pauser
                    0x...                   // yieldRecipientManager
                )
            ),
            Upgrades.DeployOptions({
                constructorData: abi.encode(mToken, swapFacility)
            })
        );
 
        vm.stopBroadcast();
 
        console.log("MyJMIToken deployed at:", proxy);
    }
}

4. Testing Your Contract

Create comprehensive tests before deployment:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
 
import { Test } from "forge-std/Test.sol";
import { MyJMIToken } from "../src/MyJMIToken.sol";
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
 
contract MyJMITokenTest is Test {
    MyJMIToken token;
 
    address admin = makeAddr("admin");
    address assetCapManager = makeAddr("assetCapManager");
    address treasury = makeAddr("treasury");
    address user = makeAddr("user");
 
    address mockUSDC;
    address mockDAI;
 
    function setUp() public {
        // Deploy mock tokens
        // Deploy your JMI token
        // Initialize with test addresses
    }
 
    function testSetAssetCap() public {
        uint256 usdcCap = 1_000_000e6; // 1M USDC
 
        vm.prank(assetCapManager);
        token.setAssetCap(mockUSDC, usdcCap);
 
        assertEq(token.assetCap(mockUSDC), usdcCap);
        assertTrue(token.isAllowedAsset(mockUSDC));
    }
 
    function testWrapWithUSDC() public {
        // Setup: Set cap and mint USDC to swap facility
        uint256 amount = 1000e6;
 
        // Test wrapping USDC through SwapFacility
        // Assert JMI tokens minted
        // Assert totalAssets increased
    }
 
    function testUnwrapLimitedToMBacking() public {
        // Setup: Wrap some M and some USDC
        // Try to unwrap more than M backing
        // Expect revert with InsufficientMBacking
    }
 
    function testReplaceAssetWithM() public {
        // Setup: Have USDC in the contract
        // Call replaceAssetWithM to swap M for USDC
        // Assert totalAssets decreased
        // Assert USDC sent to recipient
    }
 
    function testFreezing() public {
        vm.prank(admin); // Assuming admin has freeze manager role
        token.freeze(user);
        assertTrue(token.isFrozen(user));
    }
 
    function testYieldClaim() public {
        // Setup: Wrap M and let yield accrue
        // Call claimYield
        // Assert yield went to treasury
    }
}

5. Configure Supported Collateral

After deployment, the asset cap manager must configure which collateral assets are accepted:

// Set caps for each supported stablecoin
// Cap is in the asset's native decimals
 
// USDC (6 decimals) - 10M cap
myJMIToken.setAssetCap(USDC_ADDRESS, 10_000_000e6);
 
// DAI (18 decimals) - 5M cap
myJMIToken.setAssetCap(DAI_ADDRESS, 5_000_000e18);
 
// USDT (6 decimals) - 5M cap
myJMIToken.setAssetCap(USDT_ADDRESS, 5_000_000e6);

Important: Only set caps for stablecoins that maintain a reliable 1:1 peg to the dollar. The JMI model assumes 1:1 peg for all accepted collateral.

6. Gain M0 Earner Approval

For your contract to accrue yield on the $M it holds, its deployed address must be approved as an M0 Earner.

  • Calculate Your Address: Determine your contract's final deployment address using CREATE2 or by deploying to a testnet first.
  • Submit a Proposal: Create and submit a proposal to M0 Governance to add your contract's address to the earners list in the TTGRegistrar.

For more details, see the Gaining Earner Approval guide.

7. Security & Audit

Even though you are building on an audited template, any modifications should be thoroughly tested and independently audited:

  • Write comprehensive tests covering all scenarios
  • Run static analysis tools like Slither
  • Engage a reputable security firm for a full audit if you've made significant modifications

8. Deploy & Launch

Deploy to Testnet First

source .env
 
forge script script/DeployMyJMIToken.s.sol \
    --rpc-url $SEPOLIA_RPC_URL \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY \
    --broadcast

Deploy to Mainnet

source .env
 
forge script script/DeployMyJMIToken.s.sol \
    --rpc-url $MAINNET_RPC_URL \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY \
    --broadcast

Hardware Wallet Deployment

For production deployments:

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

Enable Earning

After deployment and governance approval:

myJMIToken.enableEarning();

9. Integration & Usage

Managing Asset Caps

// Check current cap
uint256 currentCap = myJMIToken.assetCap(USDC_ADDRESS);
 
// Update cap (only ASSET_CAP_MANAGER_ROLE)
myJMIToken.setAssetCap(USDC_ADDRESS, newCap);
 
// Disable an asset (set cap to 0)
myJMIToken.setAssetCap(USDC_ADDRESS, 0);

Checking Backing Status

// Total JMI supply
uint256 totalSupply = myJMIToken.totalSupply();
 
// Non-M asset backing
uint256 totalAssets = myJMIToken.totalAssets();
 
// M backing (what can be unwrapped)
uint256 mBacking = totalSupply - totalAssets;
 
// Specific asset backing
uint256 usdcBacking = myJMIToken.assetBalanceOf(USDC_ADDRESS);

Claiming Yield

// Check claimable yield
uint256 pendingYield = myJMIToken.yield();
 
// Claim yield (anyone can call, goes to yieldRecipient)
uint256 claimedAmount = myJMIToken.claimYield();

Managing Yield Recipient

// Only YIELD_RECIPIENT_MANAGER_ROLE
myJMIToken.setYieldRecipient(newTreasuryAddress);

Freezing Addresses

// Only FREEZE_MANAGER_ROLE
myJMIToken.freeze(suspiciousAddress);
myJMIToken.unfreeze(clearedAddress);
 
// Batch operations
address[] memory toFreeze = new address[](3);
toFreeze[0] = addr1;
toFreeze[1] = addr2;
toFreeze[2] = addr3;
myJMIToken.freezeAccounts(toFreeze);

Emergency Pause

// Only PAUSER_ROLE
myJMIToken.pause();  // Halts wrap, unwrap, transfer, replaceAssetWithM
myJMIToken.unpause(); // Resumes operations

Frontend Integration

// Get backing breakdown
const totalSupply = await myJMIToken.totalSupply();
const totalAssets = await myJMIToken.totalAssets();
const mBacking = totalSupply - totalAssets;
 
// Check specific asset backing
const usdcBacking = await myJMIToken.assetBalanceOf(USDC_ADDRESS);
const daisBacking = await myJMIToken.assetBalanceOf(DAI_ADDRESS);
 
// Check if wrap is allowed
const canWrap = await myJMIToken.isAllowedToWrap(USDC_ADDRESS, amount);
 
// Check if unwrap is allowed
const canUnwrap = await myJMIToken.isAllowedToUnwrap(amount);
 
// Check if asset replacement is allowed (amount in asset decimals)
const canReplace = await myJMIToken.isAllowedToReplaceAssetWithM(USDC_ADDRESS, assetAmount);
 
// Get yield info
const pendingYield = await myJMIToken.yield();
const yieldRecipient = await myJMIToken.yieldRecipient();
 
// Check account status
const isFrozen = await myJMIToken.isFrozen(userAddress);
const isPaused = await myJMIToken.paused();

10. Monitoring & Maintenance

Key Metrics to Track

MetricDescription
Total SupplyTotal JMI tokens in circulation
M BackingtotalSupply - totalAssets
Total Non-M AssetsSum of all collateral backings
Per-Asset BackingIndividual collateral levels
Asset UtilizationassetBalance / assetCap per asset
Yield RateCurrent yield accrual rate
Pending YieldUnclaimed yield amount

Operational Considerations

  1. Monitor Asset Caps: Regularly review if caps need adjustment based on:

    • Stablecoin peg stability
    • Market conditions
    • Protocol risk tolerance
  2. Rebalancing: Use replaceAssetWithM to rebalance backing when needed:

    • Arbitrageurs can swap $M for non-$M collateral
    • This naturally moves the backing toward $M over time
  3. Yield Claiming: Set up automated yield claiming if desired.

  4. Emergency Procedures:
    • Document pause/unpause procedures
    • Have freeze manager ready for suspicious activity
    • Maintain secure communication channels for emergencies
  5. Collateral Monitoring:
    • Track peg stability of accepted stablecoins
    • Be prepared to reduce/remove caps for depegged assets

Congratulations! You now have a fully functional multi-collateral stablecoin that accepts various stablecoins while automatically accumulating yield for your protocol.