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.
237 lines
11 KiB
Solidity
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);
|
|
}
|
|
}
|