ProposalManager is a governance contract built on OpenZeppelin’s Governor framework, incorporating additional logic for:
Protected Function Selectors: Certain function calls (on specific chain IDs and target addresses) are restricted to the contract owner. This prevents unauthorized calls to critical on-chain functionality within governance proposals.
Cross-Chain Awareness: Maintains a set of valid chain IDs for bridging proposals, allowing secure multi-chain governance actions when using the bridge.
Custom Quorum Rules: Allows the owner to adjust the quorum fraction within set bounds (25%–80%).
The contract also integrates with a Calldata Helper (CALLDATA_HELPER) to decode function selectors from the bridging calls. By extending Governor, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl, and Ownable, ProposalManager offers a robust on-chain DAO mechanism with time-locked execution, vote counting, adjustable quorum, and protective measures for crucial function selectors.
Governor (OpenZeppelin): Base contract for on-chain governance, offering functionalities such as proposal creation, voting, proposal states, etc.
GovernorCountingSimple (OpenZeppelin): Implements a simple vote counting mechanism: For, Against, and Abstain.
GovernorVotes (OpenZeppelin):
Integrates an IVotes token (in this case, an escrow manager providing voting power) for calculating a user’s votes at proposal snapshot blocks.
Additional External References:
ICalldataHelperV1 (CALLDATA_HELPER): A contract that decodes selectors from proposal call data, used for identifying protected function selectors in bridging calls.
EnumerableSet (OpenZeppelin): Used to store a set of valid chain IDs, ensuring efficient addition, removal, and existence checks.
MINIMUM_QUORUM_NUMERATOR (25): Minimum permissible quorum fraction (25%).
MAXIMUM_QUORUM_NUMERATOR (80): Maximum permissible quorum fraction (80%).
CALLDATA_HELPER (ICalldataHelperV1 immutable)
The address of a helper contract used to decode function selectors for bridging calls. Set in the constructor and cannot be changed.
s_bridge (address)
The address of the bridge contract used for cross-chain proposals or calls. Initially set in the constructor, can be updated by setBridge.
Parameters:
escrowManager_: An IVotes-compliant contract (e.g., an escrow manager) used for vote calculations.
timelock_: The TimelockController contract address used to queue and execute proposals after a time delay.
setBridge(address bridge_)Description:
Updates the s_bridge address used for bridging calls. Only callable by the contract owner.
Parameters:
bridge_: The new bridge contract address.
addChainId(uint256 chainId_)Description:
Adds a new chain ID to _chainIds. If the chain ID already exists, it reverts.
Checks:
_chainIds.add(chainId_) must return true, otherwise reverts with ChainIdAlreadyExists(chainId_).
removeChainId(uint256 chainId_)Description:
Removes an existing chain ID from _chainIds. If the chain ID does not exist, it reverts.
Checks:
_chainIds.remove(chainId_) must return true, otherwise reverts with ChainIdDoesNotExist(chainId_).
getChainIdsLength()Description:
Returns the number of chain IDs stored in _chainIds.
Return:
uint256: The length of _chainIds.
getChainIdAtIndex(uint256 index_)Description:
Retrieves a chain ID from _chainIds by index_.
Return:
uint256: The chain ID at the specified index.
addProtectedSelector(uint256 chainId_, address target_, bytes4 selector_)Description:
Marks a function selector (selector_) as protected on a particular chainId_ and target_. If the chain ID doesn’t exist or the selector is already protected, it reverts.
Events & Checks:
Must have _chainIds.contains(chainId_)
removeProtectedSelector(uint256 chainId_, address target_, bytes4 selector_)Description:
Removes a protected function selector from s_protectedSelectorsByChainIdAndTarget. If the chain ID isn’t recognized or the selector isn’t actually protected, it reverts.
Logic:
Ensures chainId_ is valid in _chainIds.
updateQuorumNumerator(uint256 quorumNumerator_)Description:
An override from GovernorVotesQuorumFraction that checks if the new quorum fraction is within [MINIMUM_QUORUM_NUMERATOR, MAXIMUM_QUORUM_NUMERATOR]. Otherwise reverts with GovernorInvalidQuorumFraction(...).
updateTimelock(TimelockController)Description: An override from GovernorTimelockControl. This function does nothing here (the timelock is set in the constructor).
propose(...) (Overridden from Governor)Description:
Creates a new proposal. Before creation, the contract calls _checkSelectors(...) to see if any protected calls are included in calldatas_. If protected calls are found but the proposer is not the owner, it reverts with ProtectedFunctionSelectorUsed().
After checking, it calls super.propose(...)
clock() (IERC6372)Description:
Returns the current timestamp as a uint48. Used by the governance system for time-related logic.
state(uint256 proposalId_)Description: Returns the current state of a proposal (Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, or Executed), integrating timelock considerations.
quorum(uint256 timepoint_)Description:
Calculates the number of votes required for quorum at a given timepoint_, i.e., (totalSupplyAt(timepoint_) * quorumNumerator(timepoint_)) / quorumDenominator().
CLOCK_MODE()Description:
Returns "mode=timestamp", indicating that block timestamps (instead of block numbers) are used for governance timing.
proposalThreshold(), votingDelay(), votingPeriod(), proposalNeedsQueuing(...)These standard overrides define:
proposalThreshold(): 2_500e18 — minimum voting power needed to create a proposal.
votingDelay(): 2 days — time between proposal creation and the start of voting.
_checkSelectors(...)Description:
Iterates over targets_ and associated calldatas_, extracting the function selector for each. Then checks if it is among the protected selectors for the given chain ID (block.chainid) or if the target is the s_bridge address.
_queueOperations(...), _executeOperations(...), _cancel(...), _executor()These are standard overrides from GovernorTimelockControl that handle queueing, executing, and canceling proposals in the timelock. The _executeOperations override also calls _checkSelectors(...) again before executing the proposal.
BridgeUpdated(address indexed oldBridge, address indexed newBridge)
Emitted when the bridge address changes via setBridge.
No additional custom events are defined besides those in the IProposalManager interface and standard Governor events.
From IProposalManager, we have:
ChainIdAlreadyExists(uint256 chainId)
ChainIdDoesNotExist(uint256 chainId)
SelectorAlreadyExists(bytes4 selector)
ProposalManager extends and customizes OpenZeppelin’s Governor to cater to CrossCurve DAO’s multi-chain governance needs. Through protective measures (_checkSelectors), it guards certain function calls on specific chain IDs and target addresses, ensuring only the contract owner can propose them. Additionally, it manages a dynamic range of acceptable quorum fractions and integrates with a TimelockController for secure, time-delayed proposal execution. By combining these features with ICalldataHelperV1 for decoding bridging calls, ProposalManager offers a flexible yet secure governance solution for cross-chain proposals in the CrossCurve ecosystem.
GovernorTimelockControl (OpenZeppelin):
Integrates a TimelockController for executing proposals after a governance-defined delay.
Ownable (OpenZeppelin): Provides basic ownership functionality, allowing the owner to perform restricted actions (e.g., adjusting quorum fraction, managing chain IDs/selectors).
IProposalManager (Custom): Declares additional functions, errors, and events specifically for managing cross-chain or bridging-based proposals, including protected selectors.
addChainIdremoveChainIds_protectedSelectorsByChainIdAndTarget (mapping(uint256 => mapping(address => bytes4[])))
Stores arrays of protected function selectors for each (chainId, target) pair.
s_isProtectedSelector (mapping(uint256 => mapping(address => mapping(bytes4 => bool))))
A quick boolean lookup indicating if a particular function selector is protected on a given chainId and target address.
calldataHelper_: The helper contract for decoding function selectors in bridging calls.
owner_: Address designated as the owner of this contract (can set quorum fraction, manage chain IDs, etc.).
bridge_: The initial address of the cross-chain bridge.
Logic and Effects:
Initializes the underlying Governor modules:
Governor("EYWA DAO") sets the governor name.
GovernorVotes(escrowManager_) specifies the votes token source.
GovernorVotesQuorumFraction(50) sets an initial quorum fraction of 50%.
GovernorTimelockControl(timelock_) ties this governor to the specified timelock.
Ownable(owner_) sets the contract owner.
Stores CALLDATA_HELPER and s_bridge.
Events:
BridgeUpdated(oldBridge, newBridge) logs the address change.
InvalidChainId(...)If s_isProtectedSelector[chainId_][target_][selector_] is true, reverts with SelectorAlreadyExists(...).
Adds the selector to s_protectedSelectorsByChainIdAndTarget[chainId_][target_] and sets s_isProtectedSelector[chainId_][target_][selector_] = true.
Checks if s_isProtectedSelector[chainId_][target_][selector_] == true; otherwise reverts with SelectorDoesNotExist(...).
Searches in the array s_protectedSelectorsByChainIdAndTarget[chainId_][target_], removes the entry by swapping the last element, then popping the array.
Deletes s_isProtectedSelector[chainId_][target_][selector_].
Parameters:
targets_, values_, calldatas_: Arrays specifying which contracts and functions to call, with how much ETH, plus the function data.
description_: A string describing the proposal.
Return:
uint256: The ID of the newly created proposal.
5 daysproposalNeedsQueuing(...): returns true, indicating proposals must be queued in the timelock after passing, before execution.
proposalCreator_ is not the contract owner, reverts with ProtectedFunctionSelectorUsed().Parameters:
proposalCreator_: The address that created the proposal.
targets_: The array of target contract addresses.
calldatas_: The array of encoded function calls corresponding to each target.
Logic:
If targets_[i] equals s_bridge, decodes further with CALLDATA_HELPER.decode(...) to find (m_calldata, m_target, m_chainId). Then extracts the first 4 bytes (selector) from m_calldata.
Checks s_protectedSelectorsByChainIdAndTarget[m_chainId][m_target].
If matched, marks m_isProtected = true.
Otherwise, if targets_[i] != s_bridge, reads the first 4 bytes from calldatas_[i] as the selector and checks s_protectedSelectorsByChainIdAndTarget[block.chainid][targets_[i]].
If m_isProtected == true and proposalCreator_ != owner(), revert with ProtectedFunctionSelectorUsed().
SelectorDoesNotExist(bytes4 selector)InvalidChainId(uint256 chainId)
ProtectedFunctionSelectorUsed()
GovernorInvalidQuorumFraction(uint256 newQuorumNumerator, uint256 quorumDenominator) (part of GovernorVotesQuorumFraction logic)
uint256 public constant MINIMUM_QUORUM_NUMERATOR = 25;
uint256 public constant MAXIMUM_QUORUM_NUMERATOR = 80;constructor(
IVotes escrowManager_,
TimelockController timelock_,
ICalldataHelperV1 calldataHelper_,
address owner_,
address bridge_
)
Governor("EYWA DAO")
GovernorVotes(escrowManager_)
GovernorVotesQuorumFraction(50)
GovernorTimelockControl(timelock_)
Ownable(owner_)
{
CALLDATA_HELPER = calldataHelper_;
s_bridge = bridge_;
}function setBridge(address bridge_) external onlyOwnerfunction addChainId(uint256 chainId_) external onlyOwnerfunction removeChainId(uint256 chainId_) external onlyOwnerfunction getChainIdsLength() external view returns (uint256)function getChainIdAtIndex(uint256 index_) external view returns (uint256)function addProtectedSelector(uint256 chainId_, address target_, bytes4 selector_) external onlyOwnerfunction removeProtectedSelector(uint256 chainId_, address target_, bytes4 selector_) external onlyOwnerfunction updateQuorumNumerator(uint256 quorumNumerator_) external override onlyOwnerfunction updateTimelock(TimelockController) external overridefunction propose(
address[] memory targets_,
uint256[] memory values_,
bytes[] memory calldatas_,
string memory description_
)
public
override (Governor, IGovernor)
returns (uint256)function clock() public view override (Governor, GovernorVotes, IERC6372) returns (uint48)function state(uint256 proposalId_) public view override(Governor, GovernorTimelockControl, IGovernor) returns (ProposalState)function quorum(uint256 timepoint_) public view override (Governor, GovernorVotesQuorumFraction, IGovernor) returns (uint256)function CLOCK_MODE() public pure override (Governor, GovernorVotes, IERC6372) returns (string memory)function _checkSelectors(
address proposalCreator_,
address[] memory targets_,
bytes[] memory calldatas_
)
private
view