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