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
.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. Add it to your.gitignore:echo ".env" >> .gitignore
2. Configure Your Deployment
The M0 extensions repository uses a configuration-based deployment system. You have two options:
Option A: Use Existing Deployment Scripts (Quick Start)
The repository includes ready-to-use deployment scripts. To deploy the base MYieldToOne contract:
-
Add your configuration to
script/Config.sol:
// In the _getExtensionConfig() function, add your configuration
if (chainId_ == SEPOLIA_CHAIN_ID) {
if (keccak256(bytes(name)) == keccak256(bytes("MyTreasuryToken"))) {
config.name = name;
config.symbol = "tUSD";
config.admin = 0x...; // Your admin address
config.yieldRecipient = 0x...; // Your treasury address
config.freezeManager = 0x...; // Freeze manager address
config.yieldRecipientManager = 0x...; // Yield recipient manager address
}
}- Set environment variable and deploy:
export EXTENSION_NAME="MyTreasuryToken"
export PRIVATE_KEY=0x...
forge script script/deploy/DeployYieldToOne.s.sol \
--rpc-url $SEPOLIA_RPC_URL \
--broadcast \
--verifyThe deployment script (DeployYieldToOne.s.sol) will read your configuration from Config.sol and deploy the extension.
Option B: Create Custom Deployment Script (Advanced)
If you need a completely custom deployment script or custom contract logic, write your own deployment script.
See the repository's script/deploy/ directory for examples.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.26;
import { DeployHelpers } from "../lib/common/script/deploy/DeployHelpers.sol";
import { Upgrades } from "../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol";
import { MYieldToOne } from "../src/projects/yieldToOne/MYieldToOne.sol";
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
contract DeployMyTreasuryToken is Script, DeployHelpers {
// M Token and SwapFacility addresses (same on Sepolia and Mainnet)
address constant M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b;
address constant SWAP_FACILITY = 0xB6807116b3B1B321a390594e31ECD6e0076f6278;
function run() external {
address deployer = vm.addr(vm.envUint("PRIVATE_KEY"));
vm.startBroadcast(deployer);
// Deploy implementation
address implementation = address(new MYieldToOne(M_TOKEN, SWAP_FACILITY));
// Compute deterministic salt for CREATE3
bytes32 salt = _computeSalt(deployer, "MyTreasuryToken");
// Deploy proxy via CREATE3
address proxy = _deployCreate3TransparentProxy(
implementation,
vm.envAddress("ADMIN"), // proxy admin
abi.encodeWithSelector(
MYieldToOne.initialize.selector,
"My Treasury USD", // name
"tUSD", // symbol
vm.envAddress("YIELD_RECIPIENT"), // yieldRecipient
vm.envAddress("ADMIN"), // admin
vm.envAddress("FREEZE_MANAGER"), // freezeManager
vm.envAddress("YIELD_RECIPIENT_MANAGER") // yieldRecipientManager
),
salt
);
vm.stopBroadcast();
console.log("Implementation:", implementation);
console.log("Proxy:", proxy);
console.log("ProxyAdmin:", Upgrades.getAdminAddress(proxy));
}
}Update your .env file with deployment addresses:
# Deployment configuration
ADMIN=0x...
YIELD_RECIPIENT=0x...
FREEZE_MANAGER=0x...
YIELD_RECIPIENT_MANAGER=0x...The CREATE3 deployment pattern ensures your extension has the same address on all chains when using the same deployer and salt.
3. Key Configuration Parameters
name: Token name (e.g., "My Treasury USD")symbol: Token symbol (e.g., "tUSD")yieldRecipient: Address that receives all yieldadmin: Address withDEFAULT_ADMIN_ROLE(use multisig for production)freezeManager: Address withFREEZE_MANAGER_ROLEyieldRecipientManager: Address withYIELD_RECIPIENT_MANAGER_ROLE
4. Testing Your Contract
Test your deployment using fork tests or by deploying via the transparent proxy pattern. Direct constructor + initialize calls won't work due to _disableInitializers() in the base contract.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.26;
import { Test } from "forge-std/Test.sol";
import { DeployHelpers } from "../lib/common/script/deploy/DeployHelpers.sol";
import { MYieldToOne } from "../src/projects/yieldToOne/MYieldToOne.sol";
contract MYieldToOneTest is Test, DeployHelpers {
MYieldToOne extension;
address admin = makeAddr("admin");
address treasury = makeAddr("treasury");
address user = makeAddr("user");
address constant M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b;
address constant SWAP_FACILITY = 0x34F141daCB2DeF72D2196a473C585830af8B4004;
function setUp() public {
vm.createSelectFork(vm.envString("SEPOLIA_RPC_URL"));
address deployer = makeAddr("deployer");
vm.deal(deployer, 10 ether);
vm.startPrank(deployer);
// Deploy via CREATE3 (same as production)
address implementation = address(new MYieldToOne(M_TOKEN, SWAP_FACILITY));
bytes32 salt = _computeSalt(deployer, "TestExtension");
address proxy = _deployCreate3TransparentProxy(
implementation,
admin,
abi.encodeWithSelector(
MYieldToOne.initialize.selector,
"Test Treasury USD",
"tUSD",
treasury,
admin,
admin,
admin
),
salt
);
extension = MYieldToOne(proxy);
vm.stopPrank();
}
function testYieldClaim() public {
// Enable earning first
vm.prank(admin);
extension.enableEarning();
// Test yield claiming via SwapFacility
// (requires adding extension to earners list)
}
function testFreezing() public {
vm.prank(admin);
extension.freeze(user);
assertTrue(extension.isFrozen(user));
}
function testYieldRecipientChange() public {
address newTreasury = makeAddr("newTreasury");
vm.prank(admin);
extension.setYieldRecipient(newTreasury);
assertEq(extension.yieldRecipient(), newTreasury);
}
}5. Deploy & Launch
Deploy to Testnet First
source .env
forge script script/DeployMyTreasuryToken.s.sol \
--rpc-url $SEPOLIA_RPC_URL \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY \
-vvvvThe script will output:
- Implementation address
- Proxy address (this is your extension address)
- ProxyAdmin address
Deploy to Mainnet
source .env
forge script script/DeployMyTreasuryToken.s.sol \
--rpc-url $MAINNET_RPC_URL \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEYFor production, use a hardware wallet:
forge script script/DeployMyTreasuryToken.s.sol \
--rpc-url $MAINNET_RPC_URL \
--ledger \
--broadcast \
--verifyVerify Deterministic Address
You can predict your extension address before deployment:
bytes32 salt = _computeSalt(deployer, "MyTreasuryToken");
address expectedProxy = _getCreate3Address(deployer, salt);
console.log("Expected proxy address:", expectedProxy);This address will be the same on all chains when using the same deployer and salt.
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. This is a crucial governance step.
- Calculate Your Address: Determine your contract's final deployment address. You can do this deterministically using
CREATE2or 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
earnerslist in theTTGRegistrar.
For more details on this process, see the Gaining Earner Approval guide.
Enable Earning
After deployment and governance approval, enable earning:
cast send $PROXY_ADDRESS "enableEarning()" \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $PRIVATE_KEY7. 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
8. Integration & Usage
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
$Mheld 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.

