ProposalManager
Overview
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.
Inherited Contracts and Interfaces
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.GovernorVotesQuorumFraction (OpenZeppelin): Defines quorum as a fraction of the total supply of the governance token. The fraction is adjustable by the contract’s owner within defined limits.
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.
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.
Constants
MINIMUM_QUORUM_NUMERATOR (25): Minimum permissible quorum fraction (25%).
MAXIMUM_QUORUM_NUMERATOR (80): Maximum permissible quorum fraction (80%).
State Variables
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 bysetBridge
._chainIds (EnumerableSet.UintSet)
A private set of valid chain IDs recognized by the contract. The owner can add or remove chain IDs viaaddChainId
/removeChainId
.s_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 givenchainId
andtarget
address.
Constructor
Parameters:
escrowManager_
: AnIVotes
-compliant contract (e.g., an escrow manager) used for vote calculations.timelock_
: TheTimelockController
contract address used to queue and execute proposals after a time delay.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
ands_bridge
.
External Functions
1. setBridge(address bridge_)
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.
Events:
BridgeUpdated(oldBridge, newBridge)
logs the address change.
2. addChainId(uint256 chainId_)
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 withChainIdAlreadyExists(chainId_)
.
3. removeChainId(uint256 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 withChainIdDoesNotExist(chainId_)
.
4. getChainIdsLength()
getChainIdsLength()
Description: Returns the number of chain IDs stored in
_chainIds
.Return:
uint256
: The length of_chainIds
.
5. getChainIdAtIndex(uint256 index_)
getChainIdAtIndex(uint256 index_)
Description: Retrieves a chain ID from
_chainIds
byindex_
.Return:
uint256
: The chain ID at the specified index.
6. addProtectedSelector(uint256 chainId_, address target_, bytes4 selector_)
addProtectedSelector(uint256 chainId_, address target_, bytes4 selector_)
Description: Marks a function selector (
selector_
) as protected on a particularchainId_
andtarget_
. If the chain ID doesn’t exist or the selector is already protected, it reverts.Events & Checks:
Must have
_chainIds.contains(chainId_)
, otherwiseInvalidChainId(...)
.If
s_isProtectedSelector[chainId_][target_][selector_]
is true, reverts withSelectorAlreadyExists(...)
.Adds the selector to
s_protectedSelectorsByChainIdAndTarget[chainId_][target_]
and setss_isProtectedSelector[chainId_][target_][selector_] = true
.
7. removeProtectedSelector(uint256 chainId_, address target_, bytes4 selector_)
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
.Checks if
s_isProtectedSelector[chainId_][target_][selector_] == true
; otherwise reverts withSelectorDoesNotExist(...)
.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_]
.
8. updateQuorumNumerator(uint256 quorumNumerator_)
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 withGovernorInvalidQuorumFraction(...)
.
9. updateTimelock(TimelockController)
updateTimelock(TimelockController)
Description: An override from GovernorTimelockControl. This function does nothing here (the timelock is set in the constructor).
10. propose(...)
(Overridden from Governor)
propose(...)
(Overridden from Governor)Description:
Creates a new proposal. Before creation, the contract calls
_checkSelectors(...)
to see if any protected calls are included incalldatas_
. If protected calls are found but the proposer is not the owner, it reverts withProtectedFunctionSelectorUsed()
.After checking, it calls
super.propose(...)
to proceed with the standard Governor proposal workflow.
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.
Integration with OpenZeppelin Governor Modules
clock()
(IERC6372)
clock()
(IERC6372)Description: Returns the current timestamp as a
uint48
. Used by the governance system for time-related logic.
state(uint256 proposalId_)
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_)
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()
CLOCK_MODE()
Description: Returns
"mode=timestamp"
, indicating that block timestamps (instead of block numbers) are used for governance timing.
proposalThreshold()
, votingDelay()
, votingPeriod()
, proposalNeedsQueuing(...)
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.votingPeriod()
:5 days
— how long votes are accepted.proposalNeedsQueuing(...)
: returnstrue
, indicating proposals must be queued in the timelock after passing, before execution.
Internal Functions
_checkSelectors(...)
_checkSelectors(...)
Description:
Iterates over
targets_
and associatedcalldatas_
, 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 thes_bridge
address.If any call uses a protected selector and
proposalCreator_
is not the contract owner, reverts withProtectedFunctionSelectorUsed()
.
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]
equalss_bridge
, decodes further withCALLDATA_HELPER.decode(...)
to find(m_calldata, m_target, m_chainId)
. Then extracts the first 4 bytes (selector) fromm_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 fromcalldatas_[i]
as the selector and checkss_protectedSelectorsByChainIdAndTarget[block.chainid][targets_[i]]
.If
m_isProtected == true
andproposalCreator_ != owner()
, revert withProtectedFunctionSelectorUsed()
.
_queueOperations(...)
, _executeOperations(...)
, _cancel(...)
, _executor()
_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.
Events
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.
Errors
From IProposalManager
, we have:
ChainIdAlreadyExists(uint256 chainId)
ChainIdDoesNotExist(uint256 chainId)
SelectorAlreadyExists(bytes4 selector)
SelectorDoesNotExist(bytes4 selector)
InvalidChainId(uint256 chainId)
ProtectedFunctionSelectorUsed()
GovernorInvalidQuorumFraction(uint256 newQuorumNumerator, uint256 quorumDenominator)
(part ofGovernorVotesQuorumFraction
logic)
Summary
ProposalManager extends and customizes OpenZeppelin’s Governor to cater to EYWA 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 EYWA ecosystem.
Last updated