Design Your Stablecoin

Implementation Guide: JMI (Offshore)

Step-by-step instructions for deploying a JMI (Just Mint It) stablecoin extension with multi-collateral support and centralized rewards distribution.
The JMI extension is currently available on EVM chains only (Ethereum, Base, Arbitrum, etc.).

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 rewards are captured by a designated recipient.

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

For single-collateral deployments, consider the simpler MYieldToOne implementation instead.

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    // Rewards 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 rewards (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...,                  // rewards recipient
                    0x...,                  // admin
                    0x...,                  // assetCapManager
                    0x...,                  // freezeManager
                    0x...,                  // pauser
                    0x...                   // rewards recipient manager
                )
            ),
            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 rewards accrue
        // Call claimYield
        // Assert rewards 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 rewards on the supply 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 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 earner 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 Rewards

// Check claimable rewards
uint256 pendingYield = myJMIToken.yield();

// Claim rewards (anyone can call, goes to yieldRecipient)
uint256 claimedAmount = myJMIToken.claimYield();

Managing Rewards 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 rewards 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
Rewards RateCurrent rewards accrual rate
Pending RewardsUnclaimed rewards 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 approved collateral for approved stablecoin collateral.
    • This naturally moves the backing toward approved collateral over time.
  3. Rewards Claiming: Set up automated rewards 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 rewards for your protocol.

Copyright © M0 Foundation 2026