A minimal, correct M0 Earner extension that holds $M (TestUSD) and routes 100% of accrued yield to a single yieldBeneficiary (PrizeDistributor).
MYieldToOne is an upgradeable, pausable smart contract that integrates with M0's yield earning system. It uses a pull-based claiming model where the contract must actively call M0 to claim accrued yield, then distributes 100% of that yield to a designated beneficiary.
- ✅ Pull-based yield claiming from M0
- ✅ 100% yield distribution to a single beneficiary
- ✅ Upgradeable using UUPS proxy pattern
- ✅ Pausable distribution functionality
- ✅ Role-based access control (Admin, Gov, Pauser)
- ✅ Reentrancy protection on distribution
We chose a pull-based claiming model for the following reasons:
- Simplicity: The contract explicitly controls when to claim yield, making the flow predictable
- Gas Efficiency: Only claims when needed, avoiding unnecessary transactions
- Flexibility: Can implement custom logic around claiming timing and conditions
- Transparency: Clear separation between yield accrual (M0) and distribution (this contract)
The contract implements minimal interfaces:
IMTokenLike- M0 token interface for balance and transfer operationsISwapFacility- M0 swap facility for wrapping/unwrapping M tokensIPrizeDistributor- PrizeDistributor interface for yield distribution callbacks
Exact M0 functions integrated:
IMTokenLike.balanceOf(address)- Check M token balance for yield calculationISwapFacility.wrap(uint256)- Wrap M tokens to start earningISwapFacility.unwrap(uint256)- Unwrap M tokens to stop earningIPrizeDistributor.distributeYield(uint256,uint256)- Distribute claimed yield
Claim Model: Pull-based - contract actively calls M0 to claim accrued yield, then distributes 100% to beneficiary.
We use UUPS (Universal Upgradeable Proxy Standard) because:
- More gas efficient than Transparent proxies
- Upgrade logic is in the implementation contract
- Simpler admin management
- Industry standard for upgradeable contracts
- Foundry installed
- Node.js (for OpenZeppelin contracts)
# Clone and setup
git clone <your-repo>
cd m0extension-main
forge install
# Build contracts
forge build
# Run tests
forge testRun the demo script to see the contract in action:
forge script script/Demo.s.solThis will:
- Deploy mock contracts (M0, TestUSD, PrizeDistributor)
- Initialize MYieldToOne
- Wrap 100k M tokens
- Enable earning
- Simulate 10k M yield
- Distribute yield to beneficiary
- Print before/after balances
Demo Output:
=== INITIAL BALANCES ===
Extension M token balance: 100000 M
Extension total supply: 100000 tokens
PrizeDistributor balance: 0 tokens
Claimable yield: 0 M
=== BALANCES AFTER DISTRIBUTION ===
Extension M token balance: 110000 M
Extension total supply: 110000 tokens
PrizeDistributor balance: 10000 tokens
Total yield claimed: 10000 M
Distribution successful: YES
See DEPLOY.md for complete testnet deployment guide.
Quick start:
# 1. Set environment variables
export PRIVATE_KEY="0xYourPrivateKey"
export SEPOLIA_RPC_URL="https://your-rpc-url"
# 2. Deploy
forge script script/DeployWithMocks.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast
# 3. Use one-liners to test
cast send $PROXY 'enableEarning()' --private-key $PRIVATE_KEY --rpc-url $SEPOLIA_RPC_URL
cast send $PROXY 'claimYield()' --private-key $PRIVATE_KEY --rpc-url $SEPOLIA_RPC_URL- DEFAULT_ADMIN_ROLE: Can upgrade the contract and stop earning
- GOV_ROLE: Can start earning and change beneficiary
- PAUSER_ROLE: Can pause/unpause distribution
# Run all tests
forge test
# Run with gas reporting
forge test --gas-report
# Run specific test
forge test --match-test testClaimYieldAfterWrapping
# Run with detailed output
forge test -vvvvTest Results:
╭-------------------------------------+--------+--------+---------╮
| Test Suite | Passed | Failed | Skipped |
+=================================================================+
| MYieldToPrizeDistributorTest | 53 | 0 | 0 |
|-------------------------------------+--------+--------+---------|
| MYieldToPrizeDistributorUpgradeTest | 5 | 0 | 0 |
╰-------------------------------------+--------+--------+---------╯
Total: 58 tests - 100% PASSING ✅
Test Coverage:
- ✅ Happy path: start earning, mock accrual, distribute to beneficiary
- ✅ Rotate beneficiary and distribute again
- ✅ Pause blocks distribution
- ✅ V1 to V2 upgrade path
Network: Sepolia Testnet
Block: 9368919
Contract Addresses:
- Proxy:
0x55F20C2b576Edb53B85D1e98898b53D63C8b88D2 - Implementation:
0xEFC0411F5F5Cb91A75F3ca0d2e6870da8B504484
Transaction Hashes:
- Deployment:
0x870d2067... - enableEarning:
0x3e8402b3... - claimYield:
0xc7099609...
View on Etherscan:
https://sepolia.etherscan.io/address/0x55F20C2b576Edb53B85D1e98898b53D63C8b88D2
For complete deployment guide, see DEPLOY.md
- ReentrancyGuard: Prevents reentrancy attacks on distribution
- Pausable: Can pause distribution in emergencies
- Access Control: Role-based permissions for all admin functions
- Yield Validation: Ensures yield exists before distribution
m0extension-main/
├── src/
│ ├── MYieldToPrizeDistributor.sol # Main contract (V1)
│ └── MYieldToPrizeDistributorV2.sol # Upgrade demonstration (V2)
├── interfaces/
│ ├── IMTokenLike.sol # M0 token interface
│ ├── ISwapFacility.sol # Swap facility interface
│ └── IPrizeDistributor.sol # Prize distributor interface
├── test/
│ ├── MYieldToPrizeDistributor.t.sol # Core tests
│ ├── MYieldToPrizeDistributorUpgrade.t.sol # Upgrade tests
│ └── mocks/ # Mock contracts
├── script/
│ ├── Demo.s.sol # Demo with balance tracking
│ └── DeployWithMocks.s.sol # Full deployment
├── README.md # This file
└── DEPLOY.md # Deployment guide
MIT