620 lines
22 KiB
Solidity
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"
|
|
);
|
|
}
|
|
}
|
|
}
|