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.
162 lines
7.2 KiB
Solidity
162 lines
7.2 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
|
|
|
/**
|
|
* @dev Minimal view interfaces into the CANONICAL live staking contracts on
|
|
* chain 2800. No change to either staking contract is required; both
|
|
* expose the state read here as public getters.
|
|
*
|
|
* AereStakingV2 0x1D95eF6D17aeAB732dF914Ba2d018c270BC155FC (delegated stake)
|
|
* AereLockedStaking 0x21108c28A849b05aE6b7a3a5bc435C9Bc897E7Ad (fixed-term locks)
|
|
*/
|
|
interface IAereStakingV2Votes {
|
|
function validatorCount() external view returns (uint256);
|
|
function validatorList(uint256 index) external view returns (address);
|
|
function delegations(address validator, address delegator)
|
|
external
|
|
view
|
|
returns (uint256 amount, uint256 stakedAt, uint256 lastClaimTime, uint256 rewardAccrued);
|
|
function validators(address who)
|
|
external
|
|
view
|
|
returns (
|
|
bool active,
|
|
bool exists,
|
|
uint256 selfStake,
|
|
uint256 totalDelegated,
|
|
uint256 commissionBps,
|
|
uint256 commissionAccrued
|
|
);
|
|
}
|
|
|
|
interface IAereLockedStakingVotes {
|
|
function lockCount(address user) external view returns (uint256);
|
|
function getLock(address user, uint256 lockId)
|
|
external
|
|
view
|
|
returns (uint256 amount, uint64 startedAt, uint64 unlockAt, uint8 tier, bool withdrawn);
|
|
}
|
|
|
|
/**
|
|
* @title AereStakedGovVotes (gAERE)
|
|
* @notice A THIN, NON-CUSTODIAL, CHECKPOINTED voting-power adapter over AERE's
|
|
* real staking. It is the ERC20Votes source the AereGovernor reads.
|
|
*
|
|
* WHY THIS EXISTS. The live staking contracts (AereStakingV2,
|
|
* AereLockedStaking) hold real delegated / locked AERE but do NOT expose
|
|
* ERC20Votes and are NOT checkpointed, so a raw current-balance read is
|
|
* flash-stake vote-borrowable within a single block. OpenZeppelin's
|
|
* Governor requires an IVotes source with historical snapshots
|
|
* (getPastVotes / getPastTotalSupply) so that a proposal's weight is
|
|
* fixed at a past block that an attacker cannot retroactively enter.
|
|
* This adapter provides exactly that: a checkpointed mirror of the
|
|
* correct staking source.
|
|
*
|
|
* HOW IT WORKS.
|
|
* - It CUSTODIES NOTHING. It never receives, holds, or moves AERE. The
|
|
* staked AERE stays in the staking contracts at all times.
|
|
* - gAERE balance is a pure MIRROR of an account's live staked power:
|
|
* sum of its AereStakingV2 delegations across validators, plus its
|
|
* own validator self-stake if it is a validator, plus its
|
|
* non-withdrawn AereLockedStaking locks.
|
|
* - Anyone may call sync(account) to reconcile that account's gAERE
|
|
* balance up or down to its current staked power. Each sync writes an
|
|
* ERC20Votes checkpoint, which is what makes governance weight
|
|
* snapshot-safe.
|
|
* - gAERE is NON-TRANSFERABLE (balances are derived, not spendable).
|
|
* Vote DELEGATION still works via the standard ERC20Votes delegate().
|
|
*
|
|
* HONEST SCOPE (chain 2800, today).
|
|
* AereStakingV2 currently reports validatorCount = 0 and totalStaked = 0
|
|
* and AereLockedStaking holds no live locks, so stakePowerOf() returns 0
|
|
* for every account and gAERE total supply is 0. Network-wide voting
|
|
* power is therefore 0 until real validators / delegations / locks
|
|
* exist and holders call sync() + delegate(). This adapter does not
|
|
* manufacture power; it faithfully reflects that none is staked yet.
|
|
*
|
|
* Clock: block number (OpenZeppelin default). getPastVotes at a proposal
|
|
* snapshot block is what defeats flash-stake vote borrowing.
|
|
*/
|
|
contract AereStakedGovVotes is ERC20Votes {
|
|
IAereStakingV2Votes public immutable staking; // AereStakingV2
|
|
IAereLockedStakingVotes public immutable locked; // AereLockedStaking
|
|
|
|
/// @notice Bounded scans so sync() can never be gas-bricked by an unbounded
|
|
/// validator / lock set. If a real account ever exceeds these, its
|
|
/// mirrored power is capped at the scanned window (documented, safe).
|
|
uint256 public constant MAX_VALIDATOR_SCAN = 512;
|
|
uint256 public constant MAX_LOCK_SCAN = 512;
|
|
|
|
event Synced(address indexed account, uint256 oldPower, uint256 newPower);
|
|
|
|
constructor(address stakingV2, address lockedStaking)
|
|
ERC20("AERE Staked Governance Votes", "gAERE")
|
|
ERC20Permit("AERE Staked Governance Votes")
|
|
{
|
|
require(stakingV2 != address(0) && lockedStaking != address(0), "gAERE: zero staking");
|
|
staking = IAereStakingV2Votes(stakingV2);
|
|
locked = IAereLockedStakingVotes(lockedStaking);
|
|
}
|
|
|
|
/**
|
|
* @notice Live staked voting power of `account`, read from the canonical
|
|
* staking contracts. Delegations + validator self-stake + live locks.
|
|
*/
|
|
function stakePowerOf(address account) public view returns (uint256 power) {
|
|
uint256 vc = staking.validatorCount();
|
|
if (vc > MAX_VALIDATOR_SCAN) vc = MAX_VALIDATOR_SCAN;
|
|
for (uint256 i = 0; i < vc; i++) {
|
|
address v = staking.validatorList(i);
|
|
(uint256 amt, , , ) = staking.delegations(v, account);
|
|
power += amt;
|
|
}
|
|
|
|
// If `account` is itself a validator, its live (post-slash) self-stake
|
|
// is a slashable bond and counts toward its governance weight.
|
|
(, bool exists, uint256 selfStake, , , ) = staking.validators(account);
|
|
if (exists) power += selfStake;
|
|
|
|
uint256 lc = locked.lockCount(account);
|
|
if (lc > MAX_LOCK_SCAN) lc = MAX_LOCK_SCAN;
|
|
for (uint256 j = 0; j < lc; j++) {
|
|
(uint256 lamt, , , , bool withdrawn) = locked.getLock(account, j);
|
|
if (!withdrawn) power += lamt;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Permissionlessly reconcile `account`'s gAERE balance to its live
|
|
* staked power, writing a fresh checkpoint. Callable by anyone (so
|
|
* the mirror can be kept honest, e.g. burning power after an unbond).
|
|
* The account must still self-delegate (or delegate) for the mirrored
|
|
* balance to become countable voting power.
|
|
*/
|
|
function sync(address account) public {
|
|
uint256 target = stakePowerOf(account);
|
|
uint256 cur = balanceOf(account);
|
|
if (target > cur) {
|
|
_mint(account, target - cur);
|
|
} else if (cur > target) {
|
|
_burn(account, cur - target);
|
|
}
|
|
emit Synced(account, cur, target);
|
|
}
|
|
|
|
/// @notice Convenience: sync the caller.
|
|
function syncSelf() external {
|
|
sync(msg.sender);
|
|
}
|
|
|
|
/**
|
|
* @dev Non-transferable mirror: allow only mint (from == 0) and burn
|
|
* (to == 0). Regular transfers revert. Vote delegation is unaffected
|
|
* (it moves voting units, not token balances).
|
|
*/
|
|
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
|
|
require(from == address(0) || to == address(0), "gAERE: non-transferable mirror");
|
|
super._beforeTokenTransfer(from, to, amount);
|
|
}
|
|
}
|