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

  1. 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
    }
}
  1. 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 \
    --verify

The 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 yield
  • admin: Address with DEFAULT_ADMIN_ROLE (use multisig for production)
  • freezeManager: Address with FREEZE_MANAGER_ROLE
  • yieldRecipientManager: Address with YIELD_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 \
    -vvvv

The 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_KEY

For production, use a hardware wallet:

forge script script/DeployMyTreasuryToken.s.sol \
    --rpc-url $MAINNET_RPC_URL \
    --ledger \
    --broadcast \
    --verify

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

Enable Earning

After deployment and governance approval, enable earning:

cast send $PROXY_ADDRESS "enableEarning()" \
    --rpc-url $SEPOLIA_RPC_URL \
    --private-key $PRIVATE_KEY

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

  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.