Deploying a MYieldToOne Extension
This guide provides a step-by-step walkthrough for deploying your own MYieldToOne
-based stablecoin. It assumes you have already chosen this model and understand its architecture.
If you haven't already, please review the MYieldToOne
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:
-
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 the commands provided to install dependencies and configure your development environment. -
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 extension contract will inherit from MYieldToOne
. The most important step is the initialize
function, where you configure your token's name, symbol, and access control roles.
Create a new file in the src/
directory, for example, MyTreasuryToken.sol
.
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
import { MYieldToOne } from "./projects/yieldToOne/MYieldToOne.sol";
contract MyTreasuryToken is MYieldToOne {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address mToken_, address swapFacility_) MYieldToOne(mToken_, swapFacility_) {
_disableInitializers();
}
function initialize(
string memory name,
string memory symbol,
address yieldRecipient_,
address admin,
address freezeManager,
address yieldRecipientManager
) public initializer {
MYieldToOne.initialize(
name, // "My Treasury USD"
symbol, // "tUSD"
yieldRecipient_, // Treasury wallet address
admin, // Admin multisig address
freezeManager, // Freeze manager address (can be same as admin)
yieldRecipientManager // Yield recipient manager address
);
}
}
Key Parameters Explained:
name
: The full name of your token (e.g., "My Treasury USD")symbol
: The token symbol (e.g., "tUSD")yieldRecipient_
: The wallet that will receive all yield (your treasury)admin
: Address withDEFAULT_ADMIN_ROLE
(should be a multisig)freezeManager
: Address withFREEZE_MANAGER_ROLE
yieldRecipientManager
: Address withYIELD_RECIPIENT_MANAGER_ROLE
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 { MyTreasuryToken } from "../src/MyTreasuryToken.sol";
contract DeployMyTreasuryToken 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(
"MyTreasuryToken.sol", // Contract file name
deployer, // The admin for the proxy contract itself
abi.encodeCall( // The initializer call data
MyTreasuryToken.initialize,
(
"My Treasury USD", // name
"tUSD", // symbol
0x..., // yieldRecipient
0x..., // admin (DEFAULT_ADMIN_ROLE for the logic)
0x..., // freezeManager
0x... // yieldRecipientManager
)
)
);
vm.stopBroadcast();
console.log("MyTreasuryToken deployed at:", proxy);
}
}
4. Testing Your Contract
Before deployment, create comprehensive tests:
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
import { Test } from "forge-std/Test.sol";
import { MyTreasuryToken } from "../src/MyTreasuryToken.sol";
contract MyTreasuryTokenTest is Test {
MyTreasuryToken token;
address admin = makeAddr("admin");
address treasury = makeAddr("treasury");
address user = makeAddr("user");
function setUp() public {
token = new MyTreasuryToken(
address(mockMToken), // M Token address
address(mockSwapFacility) // SwapFacility address
);
token.initialize(
"My Treasury USD",
"tUSD",
treasury, // Yield recipient
admin, // Admin
admin, // Freeze manager
admin // Yield recipient manager
);
}
function testYieldClaim() public {
// Test yield claiming functionality
// Mock some yield accrual
// Call claimYield()
// Assert yield goes to treasury
}
function testFreezing() public {
// Test freezing functionality
vm.prank(admin);
token.freeze(user);
assertTrue(token.isFrozen(user));
}
function testYieldRecipientChange() public {
// Test changing yield recipient
address newTreasury = makeAddr("newTreasury");
vm.prank(admin);
token.setYieldRecipient(newTreasury);
assertEq(token.yieldRecipient(), newTreasury);
}
}
5. 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. This is a crucial governance step.
- Calculate Your Address: Determine your contract's final deployment address. You can do this deterministically using
CREATE2
or by deploying it 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 theTTGRegistrar
.
For more details on this process, see the Gaining Earner Approval guide.
6. Security & Audit
Even though you are building on an audited template, any modifications or new logic should be thoroughly tested and independently audited. Security is paramount.
- Write comprehensive tests for your new contract in the
test/
directory. - Run static analysis tools like Slither
- Engage with a reputable security firm for a full audit if you've made significant modifications
7. Deploy & Launch
Deploy to Testnet First
# Load environment variables
source .env
# Deploy to Sepolia
forge script script/DeployMyTreasuryToken.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/DeployMyTreasuryToken.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/DeployMyTreasuryToken.s.sol \
--rpc-url $MAINNET_RPC_URL \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY \
--ledger \
--broadcast
Enable Earning
After your contract is deployed and governance approval is confirmed, call the enableEarning()
function on your deployed contract:
// Call this function to start yield accrual
myTreasuryToken.enableEarning();
This will start the yield accrual process by calling startEarning()
on the underlying M Token.
8. Integration & Usage
Interacting with the SwapFacility
To convert between $M
and your new extension, all interactions must go through the central, M0-deployed SwapFacility
contract. However, for security, direct calls from end-users to this contract are restricted.
End-users cannot call swapInM
(to wrap $M
) or swapOutM
(to unwrap to $M
) directly. The SwapFacility
will reject these calls.
Instead, your application must use a dedicated intermediary contract that you deploy. This contract is what gets "whitelisted" by M0 Governance with the M_SWAPPER_ROLE
. Your users will interact with your contract, which then securely calls the SwapFacility
on their behalf.
- You Deploy an Intermediary Contract: This contract contains your application's logic for handling deposits and withdrawals.
- You Get It Whitelisted: Through a governance proposal, your intermediary contract's address is granted the
M_SWAPPER_ROLE
. - Users Interact With Your Contract: The flow looks like this:
User
→Your Intermediary Contract
→SwapFacility
.
// This is YOUR contract that you deploy.
// Its address must be granted the M_SWAPPER_ROLE by M0 Governance.
contract MyAppInteractor {
ISwapFacility constant SWAP_FACILITY = ISwapFacility(0x...); // The central SwapFacility address
IMExtension constant YOUR_TOKEN = IMExtension(0x...); // Your extension's address
IMTokenLike constant M_TOKEN = IMTokenLike(0x...); // The $M token address
// Users call this function to deposit $M and receive your extension token.
function deposit(uint256 amount) external {
// 1. Get $M from the user.
M_TOKEN.transferFrom(msg.sender, address(this), amount);
// 2. This contract approves and calls the central SwapFacility.
M_TOKEN.approve(address(SWAP_FACILITY), amount);
SWAP_FACILITY.swapInM(address(YOUR_TOKEN), amount, msg.sender);
}
// Users call this function to burn your extension token and receive $M.
function withdraw(uint256 amount) external {
// 1. Get the extension token from the user.
YOUR_TOKEN.transferFrom(msg.sender, address(this), amount);
// 2. This contract approves and calls the central SwapFacility.
YOUR_TOKEN.approve(address(SWAP_FACILITY), amount);
SWAP_FACILITY.swapOutM(address(YOUR_TOKEN), amount, msg.sender);
}
}
For Treasury Management
-
Claiming Yield: Anyone can call the yield claim function when surplus exists:
uint256 yieldAmount = myTreasuryToken.claimYield();
-
Managing Yield Recipient: The yield recipient manager can update where yield goes:
// Only YIELD_RECIPIENT_MANAGER_ROLE can call this myTreasuryToken.setYieldRecipient(newTreasuryAddress);
-
Freezing (if needed):
// Only FREEZE_MANAGER_ROLE can call this myTreasuryToken.freeze(suspiciousAddress); myTreasuryToken.unfreeze(rehabilitatedAddress); // Batch operations address[] memory accounts = new address[](3); accounts[0] = addr1; accounts[1] = addr2; accounts[2] = addr3; myTreasuryToken.freezeAccounts(accounts);
Frontend Integration
Display real-time yield information to your users:
// Get current claimable yield
const currentYield = await myTreasuryToken.yield();
// Get total supply
const totalSupply = await myTreasuryToken.totalSupply();
// Get user balance
const userBalance = await myTreasuryToken.balanceOf(userAddress);
// Check if address is frozen
const isFrozen = await myTreasuryToken.isFrozen(userAddress);
// Get yield recipient
const yieldRecipient = await myTreasuryToken.yieldRecipient();
9. Monitoring & Maintenance
Key Metrics to Track
- Total Supply: How much of your token is in circulation
- Yield Rate: Current yield being generated
- Treasury Balance: Amount accumulated in the yield recipient address
- M Token Balance: Total
$M
held by your contract
Operational Considerations
- Monitor the
yield()
function regularly to see claimable amounts - Set up automated yield claiming if desired
- Keep track of frozen addresses for compliance
- Monitor gas costs for user operations
Congratulations! You now have a fully functional treasury-focused stablecoin that automatically accumulates yield for your protocol while providing users with a stable, 1:1 backed token experience.