aere-contracts/contracts/governance/AereGovernorV2.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

237 lines
11 KiB
Solidity

// 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);
}
}