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:
-
Clone the repository:
git clone https://github.com/m0-foundation/m-extensions.git cd m-extensions -
Follow the setup instructions: Refer to the README.md and follow all commands to install dependencies and configure your development environment.
-
Configure Environment Variables: Create a
.envfile 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-keySecurity Warning: Never commit your
.envfile 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 withDEFAULT_ADMIN_ROLE(should be a multisig)assetCapManager: Address withASSET_CAP_MANAGER_ROLE(manages collateral caps)freezeManager: Address withFREEZE_MANAGER_ROLE(can freeze addresses)pauser: Address withPAUSER_ROLE(emergency stop)yieldRecipientManager: Address withYIELD_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
CREATE2or 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
earnerslist in theTTGRegistrar.
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 \
--broadcastDeploy to Mainnet
source .env
forge script script/DeployMyJMIToken.s.sol \
--rpc-url $MAINNET_RPC_URL \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY \
--broadcastHardware Wallet Deployment
For production deployments:
forge script script/DeployMyJMIToken.s.sol \
--rpc-url $MAINNET_RPC_URL \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY \
--ledger \
--broadcastEnable 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 operationsFrontend 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
| Metric | Description |
|---|---|
| Total Supply | Total JMI tokens in circulation |
| M Backing | totalSupply - totalAssets |
| Total Non-M Assets | Sum of all collateral backings |
| Per-Asset Backing | Individual collateral levels |
| Asset Utilization | assetBalance / assetCap per asset |
| Yield Rate | Current yield accrual rate |
| Pending Yield | Unclaimed yield amount |
Operational Considerations
-
Monitor Asset Caps: Regularly review if caps need adjustment based on:
- Stablecoin peg stability
- Market conditions
- Protocol risk tolerance
-
Rebalancing: Use
replaceAssetWithMto rebalance backing when needed:- Arbitrageurs can swap
$Mfor non-$Mcollateral - This naturally moves the backing toward
$Mover time
- Arbitrageurs can swap
-
Yield Claiming: Set up automated yield claiming if desired.
-
Emergency Procedures:
- Document pause/unpause procedures
- Have freeze manager ready for suspicious activity
- Maintain secure communication channels for emergencies
-
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.

