aere-contracts/contracts/AereGovernanceStaked.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

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