// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "../vendor/openzeppelin/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.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"); bytes4 private constant SET_PRIMARY_JURISDICTION_SELECTOR = 0x997f4edf; // 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; mapping(uint256 => address) public proposalAssets; 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 mapping(uint256 => bytes32) public proposalJurisdictionIds; mapping(uint256 => bytes32) public proposalTransitionJurisdictionIds; mapping(uint256 => bool) public proposalJurisdictionReviewRequired; mapping(uint256 => uint256) public proposalMinimumNoticePeriod; mapping(uint256 => uint256) public proposalJurisdictionApprovalCount; mapping(uint256 => uint256) public proposalTransitionJurisdictionApprovalCount; mapping(uint256 => address) public proposalLastJurisdictionApprover; mapping(uint256 => address) public proposalLastTransitionJurisdictionApprover; mapping(uint256 => mapping(address => bool)) private _proposalJurisdictionApprovals; mapping(uint256 => mapping(address => bool)) private _proposalTransitionJurisdictionApprovals; 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); event ProposalJurisdictionApproved( uint256 indexed proposalId, bytes32 indexed jurisdictionId, address indexed authority, uint256 approvalCount ); event ProposalAssetScoped( uint256 indexed proposalId, address indexed asset, bytes32 indexed jurisdictionId, bool reviewRequired, uint256 minimumNoticePeriod ); /// @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 pure returns (uint256) { targets; values; calldatas; description; mode; revert("Use proposeForAsset"); } function proposeForAsset( address asset, 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"); require(assetRegistry.isAssetActive(asset), "Asset inactive"); UniversalAssetRegistry.UniversalAsset memory assetRecord = assetRegistry.getAsset(asset); bytes32 jurisdictionId = assetRegistry.getAssetJurisdictionId(asset); require(jurisdictionId != bytes32(0), "Asset jurisdiction missing"); bytes32 transitionJurisdictionId = _validateProposalScope(asset, jurisdictionId, targets, calldatas); bool reviewRequired = assetRecord.governmentApprovalRequired || assetRecord.supervisionRequired || assetRecord.requiresGovernance; uint256 minimumNoticePeriod = assetRecord.minimumUpgradeNoticePeriod; uint256 jurisdictionNoticePeriod = assetRegistry.getJurisdictionMinimumUpgradeNotice(jurisdictionId); if (jurisdictionNoticePeriod > minimumNoticePeriod) { minimumNoticePeriod = jurisdictionNoticePeriod; } if (transitionJurisdictionId != bytes32(0) && transitionJurisdictionId != jurisdictionId) { reviewRequired = true; uint256 transitionNoticePeriod = assetRegistry.getJurisdictionMinimumUpgradeNotice(transitionJurisdictionId); if (transitionNoticePeriod > minimumNoticePeriod) { minimumNoticePeriod = transitionNoticePeriod; } } 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; proposalAssets[proposalId] = asset; proposalJurisdictionIds[proposalId] = jurisdictionId; proposalTransitionJurisdictionIds[proposalId] = transitionJurisdictionId; proposalJurisdictionReviewRequired[proposalId] = reviewRequired; proposalMinimumNoticePeriod[proposalId] = minimumNoticePeriod; emit ProposalCreated( proposalId, msg.sender, targets, values, new string[](targets.length), calldatas, proposal.startBlock, proposal.endBlock, description ); emit ProposalAssetScoped( proposalId, asset, jurisdictionId, reviewRequired, minimumNoticePeriod ); 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; } function _validateProposalScope( address asset, bytes32 currentJurisdictionId, address[] memory targets, bytes[] memory calldatas ) internal view returns (bytes32 transitionJurisdictionId) { for (uint256 i = 0; i < targets.length; i++) { address target = targets[i]; bytes4 selector = _selector(calldatas[i]); if (target == asset) { if (selector == SET_PRIMARY_JURISDICTION_SELECTOR) { bytes32 derivedJurisdictionId = assetRegistry.jurisdictionIdFor( _decodePrimaryJurisdiction(calldatas[i]) ); if (transitionJurisdictionId != bytes32(0)) { require( transitionJurisdictionId == derivedJurisdictionId, "Conflicting jurisdiction transition" ); } transitionJurisdictionId = derivedJurisdictionId; } continue; } require(target == address(assetRegistry), "Unsupported asset-scoped target"); _requireRegistryCallScopedToAsset(asset, currentJurisdictionId, selector, calldatas[i]); } } function _requireRegistryCallScopedToAsset( address asset, bytes32, bytes4 selector, bytes memory data ) internal pure { if ( selector == UniversalAssetRegistry.setJurisdictionProfile.selector || selector == UniversalAssetRegistry.setJurisdictionAuthority.selector ) { revert("Use asset-derived jurisdiction registry entry point"); } require(data.length >= 36, "Registry call not asset-scoped"); address scopedAsset = _readAddressArgument(data); require(scopedAsset == asset, "Registry asset mismatch"); } function _readAddressArgument(bytes memory data) internal pure returns (address scopedAsset) { assembly { scopedAsset := and( mload(add(data, 36)), 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff ) } } function _selector(bytes memory data) internal pure returns (bytes4 selector) { require(data.length >= 4, "Invalid call data"); assembly { selector := mload(add(data, 32)) } } function _decodePrimaryJurisdiction(bytes memory data) internal pure returns (string memory jurisdiction) { jurisdiction = abi.decode(_stripSelector(data), (string)); require(bytes(jurisdiction).length > 0, "Empty jurisdiction"); } function _stripSelector(bytes memory data) internal pure returns (bytes memory payload) { require(data.length >= 4, "Invalid call data"); payload = new bytes(data.length - 4); for (uint256 i = 4; i < data.length; i++) { payload[i - 4] = data[i]; } } /** * @notice Queue a successful proposal */ function queue(uint256 proposalId) external { require(state(proposalId) == ProposalState.Succeeded, "Not succeeded"); Proposal storage proposal = proposals[proposalId]; _requireJurisdictionApprovalIfNeeded(proposalId); uint256 delay = _getTimelockDelay(proposal.mode); if (proposalMinimumNoticePeriod[proposalId] > delay) { delay = proposalMinimumNoticePeriod[proposalId]; } 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; } function approveProposalJurisdiction(uint256 proposalId) external { bytes32 jurisdictionId = proposalJurisdictionIds[proposalId]; require(jurisdictionId != bytes32(0), "Jurisdiction not derived"); require(proposalJurisdictionReviewRequired[proposalId], "Review not required"); bytes32 transitionJurisdictionId = proposalTransitionJurisdictionIds[proposalId]; bool isPrimaryAuthority = assetRegistry.isGovernanceAuthority(jurisdictionId, msg.sender); bool isTransitionAuthority = transitionJurisdictionId != bytes32(0) && transitionJurisdictionId != jurisdictionId && assetRegistry.isGovernanceAuthority(transitionJurisdictionId, msg.sender); require(isPrimaryAuthority || isTransitionAuthority, "Not jurisdiction authority"); bool approved; if (isPrimaryAuthority && !_proposalJurisdictionApprovals[proposalId][msg.sender]) { _proposalJurisdictionApprovals[proposalId][msg.sender] = true; proposalJurisdictionApprovalCount[proposalId] += 1; proposalLastJurisdictionApprover[proposalId] = msg.sender; emit ProposalJurisdictionApproved( proposalId, jurisdictionId, msg.sender, proposalJurisdictionApprovalCount[proposalId] ); approved = true; } if (isTransitionAuthority && !_proposalTransitionJurisdictionApprovals[proposalId][msg.sender]) { _proposalTransitionJurisdictionApprovals[proposalId][msg.sender] = true; proposalTransitionJurisdictionApprovalCount[proposalId] += 1; proposalLastTransitionJurisdictionApprover[proposalId] = msg.sender; emit ProposalJurisdictionApproved( proposalId, transitionJurisdictionId, msg.sender, proposalTransitionJurisdictionApprovalCount[proposalId] ); approved = true; } require(approved, "Already approved"); } function hasJurisdictionApproval(uint256 proposalId, address authority) external view returns (bool) { return _proposalJurisdictionApprovals[proposalId][authority]; } function hasTransitionJurisdictionApproval(uint256 proposalId, address authority) external view returns (bool) { return _proposalTransitionJurisdictionApprovals[proposalId][authority]; } function getProposalEta(uint256 proposalId) external view returns (uint256) { return proposals[proposalId].eta; } function _requireJurisdictionApprovalIfNeeded(uint256 proposalId) internal view { if (!proposalJurisdictionReviewRequired[proposalId]) { return; } require(proposalJurisdictionApprovalCount[proposalId] > 0, "Primary jurisdiction approval required"); bytes32 transitionJurisdictionId = proposalTransitionJurisdictionIds[proposalId]; bytes32 jurisdictionId = proposalJurisdictionIds[proposalId]; if (transitionJurisdictionId != bytes32(0) && transitionJurisdictionId != jurisdictionId) { require( proposalTransitionJurisdictionApprovalCount[proposalId] > 0, "Transition jurisdiction approval required" ); } } }