// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../registry/UniversalAssetRegistry.sol"; /** * @title GovernanceController * @notice Hybrid governance with progressive timelock based on asset risk * @dev Modes: Admin-only, 1-day timelock, 3-day + voting, 7-day + quorum */ contract GovernanceController is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE"); bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); // Governance modes enum GovernanceMode { AdminOnly, // Mode 1: Admin can execute immediately TimelockShort, // Mode 2: 1 day timelock TimelockModerate, // Mode 3: 3 days + voting required TimelockLong // Mode 4: 7 days + quorum required } // Proposal states enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed } struct Proposal { uint256 proposalId; address proposer; address[] targets; uint256[] values; bytes[] calldatas; string description; uint256 startBlock; uint256 endBlock; uint256 eta; GovernanceMode mode; ProposalState state; uint256 forVotes; uint256 againstVotes; uint256 abstainVotes; mapping(address => bool) hasVoted; } // Storage UniversalAssetRegistry public assetRegistry; mapping(uint256 => Proposal) public proposals; uint256 public proposalCount; // Governance parameters uint256 public votingDelay; // Blocks to wait before voting starts uint256 public votingPeriod; // Blocks voting is open uint256 public quorumNumerator; // Percentage required for quorum uint256 public constant TIMELOCK_SHORT = 1 days; uint256 public constant TIMELOCK_MODERATE = 3 days; uint256 public constant TIMELOCK_LONG = 7 days; uint256 public constant GRACE_PERIOD = 14 days; // Events event ProposalCreated( uint256 indexed proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description ); event VoteCast( address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason ); event ProposalQueued(uint256 indexed proposalId, uint256 eta); event ProposalExecuted(uint256 indexed proposalId); event ProposalCanceled(uint256 indexed proposalId); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize( address _assetRegistry, address admin ) external initializer { __AccessControl_init(); __ReentrancyGuard_init(); __UUPSUpgradeable_init(); require(_assetRegistry != address(0), "Zero registry"); assetRegistry = UniversalAssetRegistry(_assetRegistry); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(PROPOSER_ROLE, admin); _grantRole(EXECUTOR_ROLE, admin); _grantRole(CANCELLER_ROLE, admin); _grantRole(UPGRADER_ROLE, admin); votingDelay = 1; // 1 block votingPeriod = 50400; // ~7 days quorumNumerator = 4; // 4% quorum } function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} /** * @notice Create a new proposal */ function propose( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description, GovernanceMode mode ) external onlyRole(PROPOSER_ROLE) returns (uint256) { require(targets.length == values.length, "Length mismatch"); require(targets.length == calldatas.length, "Length mismatch"); require(targets.length > 0, "Empty proposal"); proposalCount++; uint256 proposalId = proposalCount; Proposal storage proposal = proposals[proposalId]; proposal.proposalId = proposalId; proposal.proposer = msg.sender; proposal.targets = targets; proposal.values = values; proposal.calldatas = calldatas; proposal.description = description; proposal.startBlock = block.number + votingDelay; proposal.endBlock = proposal.startBlock + votingPeriod; proposal.mode = mode; proposal.state = ProposalState.Pending; emit ProposalCreated( proposalId, msg.sender, targets, values, new string[](targets.length), calldatas, proposal.startBlock, proposal.endBlock, description ); return proposalId; } /** * @notice Cast a vote on a proposal */ function castVote( uint256 proposalId, uint8 support ) external returns (uint256) { return _castVote(msg.sender, proposalId, support, ""); } /** * @notice Cast a vote with reason */ function castVoteWithReason( uint256 proposalId, uint8 support, string calldata reason ) external returns (uint256) { return _castVote(msg.sender, proposalId, support, reason); } /** * @notice Internal vote casting */ function _castVote( address voter, uint256 proposalId, uint8 support, string memory reason ) internal returns (uint256) { Proposal storage proposal = proposals[proposalId]; require(state(proposalId) == ProposalState.Active, "Not active"); require(!proposal.hasVoted[voter], "Already voted"); require(support <= 2, "Invalid support"); uint256 weight = _getVotes(voter); proposal.hasVoted[voter] = true; if (support == 0) { proposal.againstVotes += weight; } else if (support == 1) { proposal.forVotes += weight; } else { proposal.abstainVotes += weight; } emit VoteCast(voter, proposalId, support, weight, reason); return weight; } /** * @notice Queue a successful proposal */ function queue(uint256 proposalId) external { require(state(proposalId) == ProposalState.Succeeded, "Not succeeded"); Proposal storage proposal = proposals[proposalId]; uint256 delay = _getTimelockDelay(proposal.mode); uint256 eta = block.timestamp + delay; proposal.eta = eta; proposal.state = ProposalState.Queued; emit ProposalQueued(proposalId, eta); } /** * @notice Execute a queued proposal */ function execute(uint256 proposalId) external payable nonReentrant { require(state(proposalId) == ProposalState.Queued, "Not queued"); Proposal storage proposal = proposals[proposalId]; require(block.timestamp >= proposal.eta, "Timelock not met"); require(block.timestamp <= proposal.eta + GRACE_PERIOD, "Expired"); proposal.state = ProposalState.Executed; for (uint256 i = 0; i < proposal.targets.length; i++) { _executeTransaction( proposal.targets[i], proposal.values[i], proposal.calldatas[i] ); } emit ProposalExecuted(proposalId); } /** * @notice Cancel a proposal */ function cancel(uint256 proposalId) external onlyRole(CANCELLER_ROLE) { ProposalState currentState = state(proposalId); require( currentState != ProposalState.Executed && currentState != ProposalState.Canceled, "Cannot cancel" ); Proposal storage proposal = proposals[proposalId]; proposal.state = ProposalState.Canceled; emit ProposalCanceled(proposalId); } /** * @notice Get proposal state */ function state(uint256 proposalId) public view returns (ProposalState) { Proposal storage proposal = proposals[proposalId]; if (proposal.state == ProposalState.Executed) return ProposalState.Executed; if (proposal.state == ProposalState.Canceled) return ProposalState.Canceled; if (proposal.state == ProposalState.Queued) { if (block.timestamp >= proposal.eta + GRACE_PERIOD) { return ProposalState.Expired; } return ProposalState.Queued; } if (block.number <= proposal.startBlock) return ProposalState.Pending; if (block.number <= proposal.endBlock) return ProposalState.Active; if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) { return ProposalState.Succeeded; } else { return ProposalState.Defeated; } } /** * @notice Execute transaction */ function _executeTransaction( address target, uint256 value, bytes memory data ) internal { (bool success, bytes memory returndata) = target.call{value: value}(data); if (!success) { if (returndata.length > 0) { assembly { let returndata_size := mload(returndata) revert(add(32, returndata), returndata_size) } } else { revert("Execution failed"); } } } /** * @notice Get timelock delay based on governance mode */ function _getTimelockDelay(GovernanceMode mode) internal pure returns (uint256) { if (mode == GovernanceMode.AdminOnly) return 0; if (mode == GovernanceMode.TimelockShort) return TIMELOCK_SHORT; if (mode == GovernanceMode.TimelockModerate) return TIMELOCK_MODERATE; return TIMELOCK_LONG; } /** * @notice Check if quorum is reached */ function _quorumReached(uint256 proposalId) internal view returns (bool) { Proposal storage proposal = proposals[proposalId]; if (proposal.mode == GovernanceMode.AdminOnly || proposal.mode == GovernanceMode.TimelockShort) { return true; // No quorum required } uint256 totalVotes = proposal.forVotes + proposal.againstVotes + proposal.abstainVotes; uint256 totalSupply = assetRegistry.getValidators().length; return (totalVotes * 100) / totalSupply >= quorumNumerator; } /** * @notice Check if vote succeeded */ function _voteSucceeded(uint256 proposalId) internal view returns (bool) { Proposal storage proposal = proposals[proposalId]; return proposal.forVotes > proposal.againstVotes; } /** * @notice Get voting power */ function _getVotes(address account) internal view returns (uint256) { return assetRegistry.isValidator(account) ? 1 : 0; } // Admin functions function setVotingDelay(uint256 newVotingDelay) external onlyRole(DEFAULT_ADMIN_ROLE) { votingDelay = newVotingDelay; } function setVotingPeriod(uint256 newVotingPeriod) external onlyRole(DEFAULT_ADMIN_ROLE) { votingPeriod = newVotingPeriod; } function setQuorumNumerator(uint256 newQuorumNumerator) external onlyRole(DEFAULT_ADMIN_ROLE) { require(newQuorumNumerator <= 100, "Invalid quorum"); quorumNumerator = newQuorumNumerator; } }