// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/governance/Governor.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; // Ensure TimelockController is compiled so it can be deployed by name. import "@openzeppelin/contracts/governance/TimelockController.sol"; /** * @title AereGovernorV2 * @notice A corrected OpenZeppelin Governor for AERE Network that is robust to * transient governance-supply inflation. * * WHY THIS EXISTS. The prior AereGovernor * (0xd16C2551Bf4f3d0DDF961282078917b2e8679999) used a plain * GovernorVotesQuorumFraction, so a proposal's quorum was simply * token.getPastTotalSupply(snapshot) * pct / 100, with no floor, no cap * and no time-averaging. Because AereGovLock.lock() permissionlessly * mints govAERE 1:1 from the abundant native token (2.8B supply), an * attacker could lock a large amount right before a proposal snapshot, * stay completely silent (never cast a vote), and thereby fix an * unreachable quorum. Honest holders voting unanimously For could then * never reach quorum, and the proposal would be Defeated. The attacker * could not force-pass anything (the principal is returned after the min * lock, and votes never favored a side), but could VETO every proposal. * * THE FIX. The quorum DENOMINATOR is read at a timepoint that a lock * placed after a proposal is created cannot inflate, and the result is * then clamped by an absolute floor (with an optional ceiling): * * lookbackPoint = timepoint - max(quorumLookback, votingDelay()) * supply = token.getPastTotalSupply(lookbackPoint) // clamped to 0 for early proposals * fractional = supply * quorumNumerator / quorumDenominator * quorum = max(quorumFloor, min(fractional, quorumCap)) * * WHY THE EARLIER TIMEPOINT IS THE ROBUST PART. A proposal's snapshot is * proposalCreationBlock + votingDelay. A silent whale can only react to a * proposal AFTER it exists, so its lock lands at or after the creation * block, i.e. strictly after (snapshot - votingDelay). By reading the * denominator at snapshot - max(quorumLookback, votingDelay()), which is * at or before the creation block (minus a safety margin), that reactive * lock is simply not in the measured supply. The bar therefore reflects * the supply that already existed BEFORE the proposal, so the attacker * cannot move it at all. This does not depend on choosing quorumCap * perfectly: even a very loose cap cannot be reached, because fractional * is now computed from a supply the attacker cannot inflate. * * WHY votingDelay() IS FOLDED IN. quorumLookback is a fixed deployment * constant, but votingDelay is a governable setting. Taking the max of the * two guarantees the measured point stays at or before the creation block * even if governance later raises votingDelay, so the window is always * fully covered. quorumLookback should be set to at least votingDelay plus * a small margin so that a whale front-running the propose transaction in * the same block region is also excluded. * * WHY THE FLOOR STAYS. quorumFloor bounds the requirement from BELOW so a * genuinely low-turnout proposal still fails: a small or shrunken supply * can never let a trivial amount of votes pass. quorumCap is retained as a * secondary safety ceiling (defence in depth); it is no longer the primary * defence, since the earlier-timepoint denominator already neutralizes the * veto on its own. For very early proposals, where the lookback would reach * before genesis, the measured point is clamped to 0 (supply reads 0) and * the floor governs, so quorum can never be silently zero. * * Everything else (counting, timelock execution, checkpointed vote source) * is the audited OZ reference behaviour, unchanged. The only override is * quorum(). The TimelockController is untouched and has no backdoor. All * three knobs are fixed at deployment in govAERE base units / blocks and * are publicly readable. */ contract AereGovernorV2 is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { /// @notice Absolute minimum quorum, in govAERE base units. quorum() never /// returns less than this, so a low-turnout proposal cannot pass on a /// shrunken supply. uint256 public immutable quorumFloor; /// @notice Secondary safety ceiling for quorum, in govAERE base units. quorum() /// never returns more than this. With the earlier-timepoint denominator /// this is defence in depth, not the primary anti-veto defence. uint256 public immutable quorumCap; /// @notice Blocks subtracted from a proposal's snapshot to pick the timepoint /// at which the quorum denominator (total supply) is read. It must /// cover the votingDelay window plus a small margin so that a lock /// placed after (or front-running) the proposal cannot inflate the /// denominator. At query time the effective lookback is /// max(quorumLookback, votingDelay()), so raising votingDelay later /// cannot shrink the protected window. uint256 public immutable quorumLookback; /** * @param votesToken Checkpointed IVotes source (AereGovLock). * @param timelock TimelockController that executes proposals. * @param initialVotingDelay Voting delay, in blocks. * @param initialVotingPeriod Voting period, in blocks. * @param initialProposalThreshold Proposal threshold, in vote units. * @param quorumPercent Fractional quorum, e.g. 10 = 10% of supply. * @param _quorumFloor Absolute minimum quorum (govAERE base units). * @param _quorumCap Secondary quorum ceiling (govAERE base units). * Must be >= _quorumFloor. * @param _quorumLookback Blocks before snapshot at which the quorum * denominator is read. Set to at least * initialVotingDelay plus a small margin. */ constructor( IVotes votesToken, TimelockController timelock, uint256 initialVotingDelay, uint256 initialVotingPeriod, uint256 initialProposalThreshold, uint256 quorumPercent, uint256 _quorumFloor, uint256 _quorumCap, uint256 _quorumLookback ) Governor("AereGovernorV2") GovernorSettings(initialVotingDelay, initialVotingPeriod, initialProposalThreshold) GovernorVotes(votesToken) GovernorVotesQuorumFraction(quorumPercent) GovernorTimelockControl(timelock) { require(_quorumCap >= _quorumFloor, "AereGovernorV2: cap < floor"); quorumFloor = _quorumFloor; quorumCap = _quorumCap; quorumLookback = _quorumLookback; } /** * @notice Quorum required for a proposal whose snapshot is `timepoint`. * * The denominator (total govAERE supply) is read at an EARLIER block, * timepoint - max(quorumLookback, votingDelay()), which is at or before * the proposal's creation block. A whale that locks govAERE after the * proposal exists cannot inflate that historical supply, so it cannot * raise the bar. The fractional quorum is then clamped into * [quorumFloor, quorumCap]. For an early proposal whose lookback would * reach before genesis, the point is clamped to 0 (supply reads 0) and * the floor governs. */ function quorum(uint256 timepoint) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { // Cover at least the votingDelay window; a governable votingDelay increase // can never shrink the protected lookback below its deployed value. uint256 lookback = quorumLookback; uint256 vd = votingDelay(); if (vd > lookback) { lookback = vd; } // Read the denominator at or before the proposal's creation block, clamped // to 0 for early proposals so the lookup never underflows or reverts. uint256 lookbackPoint = timepoint > lookback ? timepoint - lookback : 0; uint256 supply = token.getPastTotalSupply(lookbackPoint); // Same fraction as GovernorVotesQuorumFraction, but over a supply a late // lock cannot inflate. uint256 fractional = (supply * quorumNumerator(timepoint)) / quorumDenominator(); // Secondary ceiling, then the absolute floor from below. uint256 capped = fractional < quorumCap ? fractional : quorumCap; return capped > quorumFloor ? capped : quorumFloor; } // --------------------------------------------------------------------- // The functions below are pure Solidity multiple-inheritance overrides, // identical to AereGovernor. // --------------------------------------------------------------------- function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) { return super.votingDelay(); } function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) { return super.votingPeriod(); } function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { return super.proposalThreshold(); } function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { return super.state(proposalId); } function _execute( uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal override(Governor, GovernorTimelockControl) { super._execute(proposalId, targets, values, calldatas, descriptionHash); } function _cancel( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal override(Governor, GovernorTimelockControl) returns (uint256) { return super._cancel(targets, values, calldatas, descriptionHash); } function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { return super._executor(); } function supportsInterface(bytes4 interfaceId) public view override(Governor, GovernorTimelockControl) returns (bool) { return super.supportsInterface(interfaceId); } }