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.
204 lines
7.5 KiB
Solidity
204 lines
7.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
|
|
/**
|
|
* @dev Minimal view interface into the already-deployed AereStaking contract.
|
|
* AereStaking exposes `validatorList`, `validatorCount`, `delegations` and
|
|
* `totalStaked` as public state — no change to AereStaking is required.
|
|
*/
|
|
interface IAereStaking {
|
|
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 lastClaimBlock);
|
|
function totalStaked() external view returns (uint256);
|
|
}
|
|
|
|
/**
|
|
* @title AereGovernanceStaked
|
|
* @notice On-chain governance for AERE Network.
|
|
*
|
|
* Voting power = the total AERE a holder has STAKED via AereStaking, summed
|
|
* across every validator they delegate to. "Stake to vote": only committed
|
|
* holders govern, and the 7-day unbonding period makes vote-borrowing
|
|
* impractical. Quorum is measured against total AERE staked network-wide.
|
|
*
|
|
* Replaces the earlier AereGovernance, which read voting power from an ERC-20
|
|
* balance (only counted wrapped AERE) and depended on a getCirculatingSupply()
|
|
* method the wrapped token does not implement.
|
|
*/
|
|
contract AereGovernanceStaked is Ownable, ReentrancyGuard {
|
|
IAereStaking public immutable staking;
|
|
|
|
enum ProposalStatus { Active, Passed, Failed, Executed, Cancelled }
|
|
enum VoteType { Yes, No, Abstain }
|
|
|
|
struct Proposal {
|
|
uint256 id;
|
|
string title;
|
|
string description;
|
|
address proposer;
|
|
uint256 startTime;
|
|
uint256 endTime;
|
|
uint256 yesVotes;
|
|
uint256 noVotes;
|
|
uint256 abstainVotes;
|
|
ProposalStatus status;
|
|
uint256 executionTime;
|
|
bytes executionData;
|
|
}
|
|
|
|
struct Vote {
|
|
VoteType voteType;
|
|
uint256 votingPower;
|
|
bool hasVoted;
|
|
}
|
|
|
|
mapping(uint256 => Proposal) public proposals;
|
|
mapping(uint256 => mapping(address => Vote)) public votes;
|
|
|
|
uint256 public proposalCount;
|
|
uint256 public constant VOTING_PERIOD = 7 days;
|
|
uint256 public constant EXECUTION_DELAY = 2 days;
|
|
uint256 public constant PROPOSAL_THRESHOLD = 100000 ether; // 100k staked AERE
|
|
uint256 public constant QUORUM_PERCENTAGE = 20; // 20% of total staked
|
|
|
|
event ProposalCreated(uint256 indexed proposalId, address indexed proposer, string title);
|
|
event VoteCast(uint256 indexed proposalId, address indexed voter, VoteType voteType, uint256 votingPower);
|
|
event ProposalFinalized(uint256 indexed proposalId, ProposalStatus status);
|
|
event ProposalExecuted(uint256 indexed proposalId);
|
|
event ProposalCancelled(uint256 indexed proposalId);
|
|
|
|
constructor(address _staking) {
|
|
require(_staking != address(0), "staking=0");
|
|
staking = IAereStaking(_staking);
|
|
}
|
|
|
|
/**
|
|
* @notice Voting power of an account = total AERE it has delegated across
|
|
* all validators in AereStaking.
|
|
*/
|
|
function votingPowerOf(address account) public view returns (uint256 total) {
|
|
uint256 n = staking.validatorCount();
|
|
for (uint256 i = 0; i < n; i++) {
|
|
address v = staking.validatorList(i);
|
|
(uint256 amount, , ) = staking.delegations(v, account);
|
|
total += amount;
|
|
}
|
|
}
|
|
|
|
function createProposal(
|
|
string memory title,
|
|
string memory description,
|
|
bytes memory executionData
|
|
) external returns (uint256) {
|
|
require(votingPowerOf(msg.sender) >= PROPOSAL_THRESHOLD, "Insufficient staked AERE to propose");
|
|
|
|
proposalCount++;
|
|
uint256 proposalId = proposalCount;
|
|
proposals[proposalId] = Proposal({
|
|
id: proposalId,
|
|
title: title,
|
|
description: description,
|
|
proposer: msg.sender,
|
|
startTime: block.timestamp,
|
|
endTime: block.timestamp + VOTING_PERIOD,
|
|
yesVotes: 0,
|
|
noVotes: 0,
|
|
abstainVotes: 0,
|
|
status: ProposalStatus.Active,
|
|
executionTime: 0,
|
|
executionData: executionData
|
|
});
|
|
|
|
emit ProposalCreated(proposalId, msg.sender, title);
|
|
return proposalId;
|
|
}
|
|
|
|
function castVote(uint256 proposalId, VoteType voteType) external {
|
|
Proposal storage proposal = proposals[proposalId];
|
|
require(proposal.status == ProposalStatus.Active, "Proposal not active");
|
|
require(block.timestamp <= proposal.endTime, "Voting period ended");
|
|
require(!votes[proposalId][msg.sender].hasVoted, "Already voted");
|
|
|
|
uint256 voterPower = votingPowerOf(msg.sender);
|
|
require(voterPower > 0, "No voting power - stake AERE first");
|
|
|
|
votes[proposalId][msg.sender] = Vote({
|
|
voteType: voteType,
|
|
votingPower: voterPower,
|
|
hasVoted: true
|
|
});
|
|
|
|
if (voteType == VoteType.Yes) {
|
|
proposal.yesVotes += voterPower;
|
|
} else if (voteType == VoteType.No) {
|
|
proposal.noVotes += voterPower;
|
|
} else {
|
|
proposal.abstainVotes += voterPower;
|
|
}
|
|
|
|
emit VoteCast(proposalId, msg.sender, voteType, voterPower);
|
|
}
|
|
|
|
function finalizeProposal(uint256 proposalId) external {
|
|
Proposal storage proposal = proposals[proposalId];
|
|
require(proposal.status == ProposalStatus.Active, "Proposal not active");
|
|
require(block.timestamp > proposal.endTime, "Voting still active");
|
|
|
|
uint256 totalVotes = proposal.yesVotes + proposal.noVotes + proposal.abstainVotes;
|
|
uint256 quorumRequired = (staking.totalStaked() * QUORUM_PERCENTAGE) / 100;
|
|
|
|
if (totalVotes >= quorumRequired && proposal.yesVotes > proposal.noVotes) {
|
|
proposal.status = ProposalStatus.Passed;
|
|
proposal.executionTime = block.timestamp + EXECUTION_DELAY;
|
|
} else {
|
|
proposal.status = ProposalStatus.Failed;
|
|
}
|
|
|
|
emit ProposalFinalized(proposalId, proposal.status);
|
|
}
|
|
|
|
function executeProposal(uint256 proposalId) external nonReentrant {
|
|
Proposal storage proposal = proposals[proposalId];
|
|
require(proposal.status == ProposalStatus.Passed, "Proposal not passed");
|
|
require(block.timestamp >= proposal.executionTime, "Execution delay not met");
|
|
|
|
proposal.status = ProposalStatus.Executed;
|
|
|
|
if (proposal.executionData.length > 0) {
|
|
(bool success, ) = address(this).call(proposal.executionData);
|
|
require(success, "Execution failed");
|
|
}
|
|
|
|
emit ProposalExecuted(proposalId);
|
|
}
|
|
|
|
function cancelProposal(uint256 proposalId) external {
|
|
Proposal storage proposal = proposals[proposalId];
|
|
require(
|
|
msg.sender == proposal.proposer || msg.sender == owner(),
|
|
"Not authorized to cancel"
|
|
);
|
|
require(proposal.status == ProposalStatus.Active, "Proposal not active");
|
|
|
|
proposal.status = ProposalStatus.Cancelled;
|
|
emit ProposalCancelled(proposalId);
|
|
}
|
|
|
|
function getProposal(uint256 proposalId) external view returns (Proposal memory) {
|
|
return proposals[proposalId];
|
|
}
|
|
|
|
function getVote(uint256 proposalId, address voter) external view returns (Vote memory) {
|
|
return votes[proposalId][voter];
|
|
}
|
|
|
|
function quorumRequired() external view returns (uint256) {
|
|
return (staking.totalStaked() * QUORUM_PERCENTAGE) / 100;
|
|
}
|
|
}
|