Skip to content

Core Contracts & Epoch System Deep Dive

Component Deep Dive

Registrar (Registrar.sol)

The authoritative, onchain configuration registry for the M0 protocol. It acts as the single source of truth for parameters.

  • Functionality: Implements IRegistrar. Acts as a secure key-value store (setKey(key, value), get(key)) and manages critical address lists (addToList(list, account), removeFromList(list, account), listContains(list, account)).

  • Control: Write access (setKey, addToList, removeFromList) is strictly limited to the currently active StandardGovernor and EmergencyGovernor contracts, enforced by the onlyStandardOrEmergencyGovernor`` modifier. Read access is public.

  • Usage: Essential for protocol operation. Core contracts (like those managing MToken minting/burning, rates) read configurations from the Registrar. listContains is fundamental for permission checks (e.g., verifying if an address is an approved Minter).

  • Key Parameters Stored (Examples):


    Parameter Key String (Used to derive bytes32)DescriptionModified ByRead By (Examples)
    "minters"Whitelist of accounts allowed to mint MStandardGov/EmergencyGov (via lists)MinterGateway
    "validators"Whitelist of offchain collateral validatorsStandardGov/EmergencyGov (via lists)MinterGateway
    "earners"Whitelist for enabling M earning statusStandardGov/EmergencyGov (via lists)MToken, WrappedMToken (direct & via EarnerMgr)
    "earners_list_ignored"If set (non-zero), ignores the "earners" listStandardGov/EmergencyGov (via setKey)MToken, WrappedMToken (direct & via EarnerMgr)
    "minter_rate_model"Address of contract calculating minter interestStandardGov/EmergencyGov (via setKey)MinterGateway
    "earner_rate_model"Address of contract calculating earner interestStandardGov/EmergencyGov (via setKey)MToken
    N/A (Read via vault() func / constructor)Address of the DistributionVaultZeroGovernor (during deployment/reset)MinterGateway, Power token, Registrar
    "mint_ratio"Maximum permitted mint-to-collateral ratioStandardGov/EmergencyGov (via setKey)MinterGateway
    "update_collateral_interval"Required frequency for collateral updatesStandardGov/EmergencyGov (via setKey)MinterGateway
    "minter_freeze_time"Duration a minter is frozen upon penaltyStandardGov/EmergencyGov (via setKey)MinterGateway
    "penalty_rate"Penalty rate for infractions (e.g., missed updates)StandardGov/EmergencyGov (via setKey)MinterGateway
    "update_collateral_threshold"Number of validator signatures neededStandardGov/EmergencyGov (via setKey)MinterGateway
    "mint_delay"Delay before a mint proposal is executableStandardGov/EmergencyGov (via setKey)MinterGateway
    "mint_ttl"Time-to-live for a mint proposal after delayStandardGov/EmergencyGov (via setKey)MinterGateway
    "base_minter_rate"Base rate for minter interestStandardGov/EmergencyGov (via setKey)MinterRateModel
    "max_earner_rate"Max rate for earner interestStandardGov/EmergencyGov (via setKey)EarnerRateModel
    "em_admins"List of admins allowed to manage earners via EarnerManagerStandardGov/EmergencyGov (via lists)EarnerManager

DistributionVault (DistributionVault.sol)

Aggregates and distributes various protocol revenues to Zero token holders.

  • Revenue Sources: Designed to receive arbitrary ERC20 tokens. Key expected sources include:
    • Excess M yield generated by the protocol (must be transferred externally to the Vault's address).
    • CashToken proceeds from Power token Dutch auctions (transferred automatically by Power token.buy).
    • Unrefunded CashToken proposal fees from failed/expired StandardGovernor proposals (transferred by ``StandardGovernor.sendProposalFeeToVault).
  • Distribution Accounting (Critical Process):
    1. Trigger: The distribute(token) function must be called (permissionlessly, by any external account) for the Vault to recognize newly received balances of a specific token.
    2. Calculation: It measures the increase in the Vault's balance of token since the last call (IERC20(token).balanceOf(address(this)) - _lastTokenBalances[token]).
    3. Recording: This difference (the newly distributable amount) is added to the mapping distributionOfAt[token][currentEpoch], earmarking it for ZERO holders of the current epoch.
    4. State Update: _lastTokenBalances[token] is updated to the current balance.
  • Claiming Mechanism:
    • Zero token holders call claim(token, startEpoch, endEpoch, destination) or its gasless signature variant claimBySig(...).
    • The function processes epochs strictly in the past (endEpoch < currentEpoch). It reverts if the range includes the current epoch or future epochs.
    • It iterates from startEpoch to endEpoch. For each epoch i:
      • It retrieves the claimant's historical ZERO balance (claimantBalance) and the total ZERO supply (epochTotalSupply) for epoch i using IZero token(zeroToken).pastBalancesOf(account, i, i) and IZero token(zeroToken).pastTotalSupplies(i, i).
      • It calculates the pro-rata share for that epoch: share = (distributionOfAt[token][i] * claimantBalance * _GRANULARITY) / epochTotalSupply. The _GRANULARITY constant (1e9) is used during accumulation to minimize precision loss from integer division.
      • It checks the hasClaimed[token][i][account] mapping and skips the epoch if already claimed.
    • After iterating, it marks all processed epochs (startEpoch to endEpoch) as claimed in the hasClaimed mapping for that token and account.
    • The final accumulated claimed_ amount (scaled by _GRANULARITY) is divided by _GRANULARITY to get the true token amount, _lastTokenBalances[token] is decremented, and the tokens are transferred to the destination.
  • State: Maintains _lastTokenBalances (internal balance tracking), distributionOfAt (per-token, per-epoch distributable amounts), and hasClaimed (per-token, per-epoch, per-user claim status) mappings. More about the overall place of the Distribution Vault in the protocol in its dedicated section

StandardGovernor (StandardGovernor.sol)

Manages the regular, day-to-day governance process with economic incentives.

  • Allowed Proposal Actions: Limited to calling specific functions on itself which then interact with the Registrar or its own settings: addToList, removeFromList, removeFromAndAddToList, setKey, and setProposalFee. This prevents proposals from executing arbitrary code. Enforced by onlySelf modifier and internal _revertIfInvalidCalldata check.
  • Proposal Fees: Requires a proposalFee (set via governance, stored in proposalFee state variable, initially set in constructor) paid in the currently active CashToken (cashToken state variable). The fee is transferred from the proposer using transferFrom when propose is called. It is refunded via transfer upon successful execution (execute), or can be optionally sent to the DistributionVault via sendProposalFeeToVault if the proposal is Defeated or Expired. The cashToken and proposalFee can be changed by the ZeroGovernor via setCashToken.
  • Voting Mechanism: Uses POWER tokens. Success requires a simple majority (Yes votes > No votes), checked within the state() function logic (specifically proposal_.yesWeight > proposal_.noWeight when epoch > voteEnd). Voting is strictly confined to the designated Voting Epoch (odd-numbered epoch N+1 following the proposal epoch N).
  • Proposal Lifecycle:
    1. Propose: During a Transfer Epoch (N). Pays fee. Records proposal details. Increments numberOfProposalsAt[N+1]. If it's the first proposal for epoch N+1, triggers Power token.markNextVotingEpochAsActive.
    2. Vote: During the subsequent Voting Epoch (N+1). state() is Active. POWER transfers/delegations disabled. Voters call castVote or castVotes.
    3. Tally & Outcome: At the start of Epoch N+2 (Transfer Epoch), the outcome is determined based on votes tallied at the end of Epoch N+1. state() becomes Succeeded or Defeated.
    4. Execute: If Succeeded, can be executed by anyone calling execute() during Epoch N+2. Executes the proposed action (e.g., setKey on Registrar). Refunds fee to proposer.
    5. Expire/Send Fee: If Defeated, or Succeeded but not executed during Epoch N+2, becomes Expired. Fee can then be sent to DistributionVault via sendProposalFeeToVault.
  • Reward Distribution: Tracks participation per voter per epoch (numberOfProposalsVotedOnAt, numberOfProposalsAt). If hasVotedOnAllProposals returns true for a voter (voter_) after they cast their final vote in an epoch (checked inside _castVote), the contract calls Power token.markParticipation(voter_) (to enable POWER inflation for the delegatee) and Zero token.mint(voter_, reward) (to distribute ZERO reward to the delegatee). The reward calculation uses maxTotalZeroRewardPerActiveEpoch.

EmergencyGovernor (EmergencyGovernor.sol)

Provides an expedited path for critical governance actions using threshold consensus.

  • Allowed Proposal Actions: Similar restricted scope as StandardGovernor, targeting itself to interact with the Registrar or StandardGovernor: addToList, removeFromList, removeFromAndAddToList, setKey, setStandardProposalFee. Enforced by onlySelf and _revertIfInvalidCalldata.
  • Proposal Fees: None required.
  • Voting Mechanism: Uses POWER tokens. Requires a high threshold (thresholdRatio, e.g., 6500 basis points = 65%) of the total POWER supply at the snapshot block (voteStart - 1). The required number of votes is calculated by proposalQuorum(). Success determined in state() by checking yesWeight >= quorum(). Faster voting window: voteEnd = voteStart + 1, meaning voting spans the proposal epoch and the subsequent one.
  • Proposal Lifecycle:
    1. Propose: Can be proposed in any epoch (N). Records proposal details including the current thresholdRatio.
    2. Vote: Voting is active during Epoch N and Epoch N+1. Voters call castVote or castVotes.
    3. Execute: Can be executed by anyone calling execute() as soon as the state becomes Succeeded (i.e., threshold is met), up until the end of Epoch N+1. proposalDeadline is end of Epoch N+1. Executes the proposed action.
    4. Outcome: If not executed by end of Epoch N+1, becomes Expired. If threshold never met, remains Defeated.
  • No Direct Incentives: Does not trigger Power token inflation or Zero token reward minting. Its use is reserved for genuine emergencies. The threshold ratio can be changed by the ZeroGovernor.

ZeroGovernor (ZeroGovernor.sol)

The apex governor, controlling the structure of the governance system itself via threshold consensus.

  • Allowed Proposal Actions: Limited to highly impactful, predefined functions targeting itself or the governors it controls:
    • resetToPowerHolders(): Redeploys Power token, StandardGovernor, EmergencyGovernor via their respective Deployer contracts, bootstrapping the new Power token from the old Power token balances.
    • resetToZeroHolders(): Redeploys the same contracts, but bootstraps the new Power token from the current Zero token balances.
    • setCashToken(address newCashToken, uint256 newProposalFee): Changes the designated CashToken used for StandardGovernor fees and Power token auctions (must be pre-approved in _allowedCashTokens mapping, set at deployment). Also sets the StandardGovernor fee simultaneously via an internal call.
    • setEmergencyProposalThresholdRatio(uint16 newThresholdRatio): Adjusts the voting threshold for the EmergencyGovernor via an internal call.
    • setZeroProposalThresholdRatio(uint16 newThresholdRatio): Adjusts the voting threshold for the ZeroGovernor itself via _setThresholdRatio.
  • Proposal Fees: None required.
  • Voting Mechanism: Uses ZERO tokens. Requires a threshold (thresholdRatio) percentage of the total ZERO supply at the snapshot block (voteStart - 1). proposalQuorum() calculates required votes. Success determined in state() by checking yesWeight >= quorum(). Faster voting window: voteEnd = voteStart + 1.
  • Proposal Lifecycle: Same faster lifecycle as EmergencyGovernor: Propose (N) -> Vote (N & N+1) -> Execute (anytime during N or N+1, once threshold met). proposalDeadline is end of Epoch N+1.
  • Reset Functionality: The resetTo* functions call the deploy() method on the relevant deployer contracts (Power tokenDeployer, StandardGovernorDeployer, EmergencyGovernorDeployer). These deployers use CREATE to deterministically deploy new contract instances. The ZeroGovernor emits a ResetExecuted event logging the addresses of the newly deployed contracts and the bootstrap token used. This is the designated mechanism for major governance upgrades or system recovery.

Epoch System (PureEpochs.sol)

Overview

The entire governance system operates on a strict, time-based epoch system defined in the PureEpochs.sol library. This structures governance activities and enhances security by preventing certain actions during sensitive periods.

  • Epoch Length: Fixed at 15 days (EPOCH_PERIOD = 15 days).
  • Starting Timestamp: Epoch 1 commenced on September 15, 2022, 06:42:42 AM UTC (STARTING_TIMESTAMP = 1_663_224_162). The current epoch can be calculated based on block.timestamp.
  • Clock Mode: Conforms to ERC-6372 standard, defined by PureEpochs.clockMode() (which encodes the starting timestamp and period).
  • Epoch Types: Epochs alternate between two types:
    • Transfer Epochs (Even-numbered Epochs):
      • Activity: Power tokens can be transferred and delegated. Power token Dutch auctions are active (if supply available). StandardGovernor proposals can be created targeting the next Voting Epoch.
    • Voting Epochs (Odd-numbered Epochs):
      • Restrictions: Power token transfers and delegations are disabled by the notDuringVoteEpoch modifier in Power token to ensure stable voting power for the duration. Power token Dutch auctions are inactive.
      • Activity: StandardGovernor proposals created in the previous Transfer Epoch can be voted on. Power token inflation and Zero token rewards are calculated and distributed by the respective contracts based on voting participation within this specific epoch.
    • Note on Emergency/Zero Governors: Their faster proposal lifecycle (voteEnd = voteStart + 1) means voting can span across the boundary between any two consecutive epochs (one Transfer, one Voting, or vice-versa), regardless of type restrictions on Power token transfers. The system uses simple functions in PureEpochs.sol like currentEpoch() and timeRemainingInCurrentEpoch() to manage time. This alternating Transfer/Voting epoch structure is crucial for the integrity of StandardGovernor voting, preventing vote manipulation through last-minute Power token movements.

Epoch-Based Token Tracking

Both Power token and Zero token inherit from EpochBasedVoteToken, which uses a sophisticated snapshot system to track balances, total supply, voting power, and delegations historically across epochs. Key structures include:

// Stores an amount valid from a specific epoch onwards
struct AmountSnap {
    uint16 startingEpoch;
    uint240 amount;
}
 
// Stores an address valid from a specific epoch onwards
struct AccountSnap {
    uint16 startingEpoch;
    address account;
}
 
// Storage patterns (simplified representation)
mapping(address => AmountSnap[]) internal _balances; // Balance snaps per account
mapping(address => AccountSnap[]) internal _delegatees; // Delegatee snaps per delegator
mapping(address => AmountSnap[]) internal _votingPowers; // Voting power snaps per delegatee
AmountSnap[] internal _totalSupplies; // Total supply snaps

This allows the system to efficiently query any past state using functions like getPastVotes, pastBalanceOf, and pastTotalSupplies, which are essential for:

  1. Determining voting weight for proposals based on past snapshots.
  2. Calculating fair distribution in the DistributionVault.
  3. Enabling the inflation (_getUnrealizedInflation) and auction (_getTotalSupply(epoch - 1)) mechanisms in Power token.
  4. Calculating ZERO rewards in StandardGovernor (_getTotalSupply(epoch - 1)).

Vote Batching

All three governors (StandardGovernor, EmergencyGovernor, ZeroGovernor) inherit from BatchGovernor and support vote batching to enable efficient multi-proposal voting in a single transaction:

// Cast votes for multiple proposals
function castVotes(
    uint256[] calldata proposalIds,
    uint8[] calldata supportList
) external returns (uint256 weight);
 
// Cast votes with reasons for multiple proposals
function castVotesWithReason(
    uint256[] calldata proposalIds,
    uint8[] calldata supportList,
    string[] calldata reasonList_
) external returns (uint256 weight);
 
// Gasless signature variants also exist (e.g., castVotesBySig)

This reduces gas costs and improves the user experience for active participants.