Skip to content

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:

  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    // 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 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...,                    // 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 the TTGRegistrar.

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.

Key Concept: Whitelisted Swapper Contracts

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.

Correct Integration Pattern:
  1. You Deploy an Intermediary Contract: This contract contains your application's logic for handling deposits and withdrawals.
  2. You Get It Whitelisted: Through a governance proposal, your intermediary contract's address is granted the M_SWAPPER_ROLE.
  3. Users Interact With Your Contract: The flow looks like this: UserYour Intermediary ContractSwapFacility.
Example User Flow (Simplified):
// 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

  1. Claiming Yield: Anyone can call the yield claim function when surplus exists:

    uint256 yieldAmount = myTreasuryToken.claimYield();
  2. Managing Yield Recipient: The yield recipient manager can update where yield goes:

    // 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 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.