Implementation Guide: JMI (Offshore)
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.
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:Security Warning: Never commit your# 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.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 // 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 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..., // 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
CREATE2or by deploying to a testnet first. - Submit a Proposal: Create and submit a proposal 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 \
--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
| 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 |
| Rewards Rate | Current rewards accrual rate |
| Pending Rewards | Unclaimed rewards 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 approved collateral for approved stablecoin collateral.
- This naturally moves the backing toward approved collateral over time.
- Rewards Claiming: Set up automated rewards 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 rewards for your protocol.
Implementation Guide: MYieldToOne (Onshore)
Step-by-step instructions for deploying a Treasury Model stablecoin where 100% of rewards are captured by the use case owner.
Getting started
Deep dive into the JMI ("Just Mint It") extension template which accepts multiple collateral types while directing 100% of rewards to a single recipient.