Design Your Stablecoin

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.

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.

This guide covers the standard 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:

  1. Clone the repository:
    git clone https://github.com/m0-foundation/m-extensions.git
    cd m-extensions
    
  2. Follow the setup instructions: Refer to the README.md and follow all the commands provided to install dependencies and configure your development environment.
  3. 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    // 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 with DEFAULT_ADMIN_ROLE (should be a multisig)
  • freezeManager: Address with FREEZE_MANAGER_ROLE
  • yieldRecipientManager: Address with YIELD_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 CREATE2 or 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 earners list.

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

  1. Claiming Rewards: Anyone can call the rewards claim function when surplus exists:
    uint256 yieldAmount = myTreasuryToken.claimYield();
    
  2. Managing Rewards Recipient: The rewards recipient manager can update where rewards go:
    // Only YIELD_RECIPIENT_MANAGER_ROLE can call this
    myTreasuryToken.setYieldRecipient(newTreasuryAddress);
    
  3. 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.

Copyright © M0 Foundation 2026