Implementation Guide: MYieldToOne (Onshore)
Deploy a Treasury Model stablecoin where 100% of rewards are captured by the use case owner. This guide covers the EVM implementation using MYieldToOne.
If you haven't already, please review the Treasury Model Deep Dive for conceptual details about this model and its implementations.
MYieldToOne implementation. If you need to accept additional stablecoins (USDC, USDT, DAI), see the JMI Implementation Guide instead.EVM Implementation: MYieldToOne
This section covers deploying the MYieldToOne contract on EVM chains (Ethereum, Base, Arbitrum, etc.).
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: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. 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 // Rewards 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 rewards (your treasury)admin: Address withDEFAULT_ADMIN_ROLE(should be a multisig)freezeManager: Address withFREEZE_MANAGER_ROLEyieldRecipientManager: 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..., // rewards recipient
0x..., // admin (DEFAULT_ADMIN_ROLE for the logic)
0x..., // freezeManager
0x... // rewards recipient manager
)
)
);
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, // Rewards recipient
admin, // Admin
admin, // Freeze manager
admin // Rewards recipient manager
);
}
function testYieldClaim() public {
// Test rewards claiming functionality
// Mock some rewards accrual
// Call claimYield()
// Assert rewards goes to treasury
}
function testFreezing() public {
// Test freezing functionality
vm.prank(admin);
token.freeze(user);
assertTrue(token.isFrozen(user));
}
function testYieldRecipientChange() public {
// Test changing rewards 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 rewards, its deployed address must be approved as an M0 Earner.
- 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 to add your contract's address to the
earnerslist.
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 earner approval is confirmed, call the enableEarning() function on your deployed contract:
// Call this function to start rewards accrual
myTreasuryToken.enableEarning();
This will start the rewards accrual process.
8. Integration & Usage
For Treasury Management
- Claiming Rewards: Anyone can call the rewards claim function when surplus exists:
uint256 yieldAmount = myTreasuryToken.claimYield(); - Managing Rewards Recipient: The rewards recipient manager can update where rewards go:
// 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 rewards information to your users:
// Get current claimable rewards
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 rewards recipient
const yieldRecipient = await myTreasuryToken.yieldRecipient();
9. Monitoring & Maintenance
Key Metrics to Track
- Total Supply: How much of your token is in circulation.
- Rewards Rate: Current rewards being generated.
- Treasury Balance: Amount accumulated in the rewards recipient address.
- Underlying Balance: Total value held by your contract.
Operational Considerations
- Monitor the
yield()function regularly to see claimable amounts. - Set up automated rewards 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 rewards for your use case while providing users with a stable, 1:1 backed token experience.
SVM Implementation
For the Solana/SVM implementation, see the dedicated NoYield (SVM) Deployment Guide.