Files
smom-dbis-138/contracts/governance/GovernanceController.sol

620 lines
22 KiB
Solidity

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