Governance
On-chain DAO governance with PRIVGovernor and PRIVTimelock for protocol decisions.
Overview
PRIV Protocol governance consists of two contracts: PRIVGovernor for proposal management and voting, and PRIVTimelock for delayed execution. Together, they enable decentralized control over protocol parameters.
PRIVGovernor
| Property | Value |
|---|---|
| Voting Delay | 1 day (~43,200 blocks) |
| Voting Period | 7 days (~302,400 blocks) |
| Proposal Threshold | 100,000 PRIV |
| Quorum | 4% of total supply |
| Proposal Cooldown | 1 day between proposals |
| Max Active Proposals | 3 per address |
PRIVTimelock
| Property | Value |
|---|---|
| Default Min Delay | 2 days |
| Role: Proposer | PRIVGovernor |
| Role: Executor | Open (address(0)) |
| Role: Canceller | PRIVGovernor |
Governance Flow
1. Create Proposal (100K PRIV required)
|
v
2. Voting Delay (1 day)
|
v
3. Voting Period (7 days) - For/Against/Abstain
|
v
4. Queue in Timelock (if passed + quorum)
|
v
5. Timelock Delay (2 days)
|
v
6. Execute ProposalPRIVGovernor
Key Features
- OpenZeppelin Governor: Battle-tested governance framework
- Token-Based Voting: Vote weight from PRIV token (ERC20Votes)
- Spam Protection: Cooldown and proposal limits per address
- Timelock Integration: All executions go through PRIVTimelock
- 4% Quorum: Percentage of total supply required to pass
Spam Protection
The governor includes anti-spam measures:
- Proposal Cooldown: 1 day minimum between proposals from same address
- Active Proposal Limit: Maximum 3 active proposals per address
- Proposal Threshold: Need 100,000 PRIV to create proposals
Functions
propose
Create a new governance proposal.
function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) public override returns (uint256)| Parameter | Type | Description |
|---|---|---|
targets | address[] | Contract addresses to call |
values | uint256[] | ETH values for each call |
calldatas | bytes[] | Encoded function calls |
description | string | Proposal description |
Requirements:
- Caller must have >= 100,000 PRIV voting power
- Caller must not be in cooldown period (1 day)
- Caller must have < 3 active proposals
Returns: The proposal ID.
castVote / castVoteWithReason
Cast a vote on a proposal.
function castVote(uint256 proposalId, uint8 support) external returns (uint256)
function castVoteWithReason(uint256 proposalId, uint8 support, string calldata reason) external returns (uint256)| Support Value | Meaning |
|---|---|
| 0 | Against |
| 1 | For |
| 2 | Abstain |
queue
Queue a successful proposal in the timelock.
function queue(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public returns (uint256)Requirements:
- Proposal must have passed (For > Against)
- Quorum must be met (4% of total supply)
- Voting period must be ended
execute
Execute a queued proposal after timelock delay.
function execute(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public payable returns (uint256)Requirements:
- Timelock delay must have elapsed (2 days)
cancel
Cancel a proposal (only proposer or if threshold is no longer met).
function cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public returns (uint256)cleanupProposal
Clean up a finalized proposal (Defeated/Expired) to decrement active count.
function cleanupProposal(uint256 proposalId) externalNote: Call this for defeated/expired proposals to free up your active proposal slot.
View Functions
canPropose
Check if an account can create a proposal.
function canPropose(address account) external view returns (bool)Checks: voting power threshold, cooldown, and active proposal limit.
proposalCooldownRemaining
Get remaining cooldown time for an account.
function proposalCooldownRemaining(address account) external view returns (uint256)Returns seconds remaining, or 0 if no cooldown.
proposalProposer
Get the proposer address for a proposal.
function proposalProposer(uint256 proposalId) public view returns (address)state
Get the current state of a proposal.
function state(uint256 proposalId) public view returns (ProposalState)ProposalState enum:
Pending- Waiting for voting to startActive- Voting in progressCanceled- Proposal was canceledDefeated- Voting ended, did not passSucceeded- Voting ended, passedQueued- Queued in timelockExpired- Timelock period expiredExecuted- Successfully executed
proposalThreshold
Get the minimum tokens required to create a proposal.
function proposalThreshold() public view returns (uint256)Returns: 100,000 * 10^18 (100K PRIV)
quorum
Get the quorum required at a block number.
function quorum(uint256 blockNumber) public view returns (uint256)Returns: 4% of total supply at that block.
PRIVTimelock
The timelock adds a mandatory delay between proposal approval and execution, giving users time to react to governance decisions.
Roles
| Role | Purpose | Default Holder |
|---|---|---|
| PROPOSER_ROLE | Can queue operations | PRIVGovernor |
| EXECUTOR_ROLE | Can execute operations | address(0) (anyone) |
| CANCELLER_ROLE | Can cancel operations | PRIVGovernor |
| DEFAULT_ADMIN_ROLE | Can grant/revoke roles | Initial deployer (should renounce) |
Functions
schedule
Schedule an operation (usually called by Governor).
function schedule(
address target,
uint256 value,
bytes calldata data,
bytes32 predecessor,
bytes32 salt,
uint256 delay
) public onlyRole(PROPOSER_ROLE)execute
Execute a scheduled operation after delay.
function execute(
address target,
uint256 value,
bytes calldata data,
bytes32 predecessor,
bytes32 salt
) public payable onlyRoleOrOpenRole(EXECUTOR_ROLE)cancel
Cancel a scheduled operation.
function cancel(bytes32 id) public onlyRole(CANCELLER_ROLE)getMinDelay
Get the minimum delay for operations.
function getMinDelay() public view returns (uint256)Returns: 2 days (172800 seconds)
Events
PRIVGovernor
/// @notice Emitted when a proposal is created
event ProposalCreated(
uint256 proposalId,
address proposer,
address[] targets,
uint256[] values,
string[] signatures,
bytes[] calldatas,
uint256 voteStart,
uint256 voteEnd,
string description
);
/// @notice Emitted when a vote is cast
event VoteCast(
address indexed voter,
uint256 proposalId,
uint8 support,
uint256 weight,
string reason
);
/// @notice Emitted when a proposal is executed
event ProposalExecuted(uint256 proposalId);
/// @notice Emitted when a proposal is canceled
event ProposalCanceled(uint256 proposalId);PRIVTimelock
/// @notice Emitted when an operation is scheduled
event CallScheduled(
bytes32 indexed id,
uint256 indexed index,
address target,
uint256 value,
bytes data,
bytes32 predecessor,
uint256 delay
);
/// @notice Emitted when an operation is executed
event CallExecuted(
bytes32 indexed id,
uint256 indexed index,
address target,
uint256 value,
bytes data
);
/// @notice Emitted when an operation is cancelled
event Cancelled(bytes32 indexed id);Errors
PRIVGovernor
/// @notice Thrown when proposer is still in cooldown period
error ProposalCooldownActive(uint256 remainingTime);
/// @notice Thrown when proposer has too many active proposals
error TooManyActiveProposals(uint256 current, uint256 max);
/// @notice Thrown when trying to cleanup a non-finalized proposal
error ProposalNotFinalized(uint256 proposalId);
/// @notice Thrown when proposal has already been cleaned up
error ProposalAlreadyCleaned(uint256 proposalId);Usage Examples
Delegate Voting Power
Before participating in governance, you must delegate your voting power:
import { useWriteContract } from 'wagmi'
// Delegate to yourself
async function delegateToSelf() {
await writeContract({
address: PRIV_TOKEN_ADDRESS,
abi: privTokenAbi,
functionName: 'delegate',
args: [myAddress],
})
}
// Delegate to someone else
async function delegateTo(delegatee: string) {
await writeContract({
address: PRIV_TOKEN_ADDRESS,
abi: privTokenAbi,
functionName: 'delegate',
args: [delegatee],
})
}Create a Proposal
import { encodeFunctionData, keccak256, toBytes } from 'viem'
async function createProposal() {
// Example: Proposal to update protocol fee to 3%
const calldata = encodeFunctionData({
abi: dataXchangeAbi,
functionName: 'setProtocolFee',
args: [300n], // 3% = 300 basis points
})
const description = `# Update Protocol Fee to 3%
## Summary
Increase the DataXchange protocol fee from 2.5% to 3% to fund development.
## Rationale
- Increased development costs
- Need for additional security audits
- Community fund expansion
`
await writeContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'propose',
args: [
[DATAXCHANGE_ADDRESS], // targets
[0n], // values (no ETH)
[calldata], // calldatas
description, // description
],
})
}Vote on a Proposal
async function voteFor(proposalId: bigint) {
await writeContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'castVoteWithReason',
args: [proposalId, 1, 'I support this proposal because...'],
})
}
async function voteAgainst(proposalId: bigint) {
await writeContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'castVoteWithReason',
args: [proposalId, 0, 'I oppose this proposal because...'],
})
}Queue and Execute
import { keccak256, toBytes } from 'viem'
async function queueProposal(
targets: string[],
values: bigint[],
calldatas: `0x${string}`[],
description: string
) {
const descriptionHash = keccak256(toBytes(description))
await writeContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'queue',
args: [targets, values, calldatas, descriptionHash],
})
}
async function executeProposal(
targets: string[],
values: bigint[],
calldatas: `0x${string}`[],
description: string
) {
const descriptionHash = keccak256(toBytes(description))
await writeContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'execute',
args: [targets, values, calldatas, descriptionHash],
})
}Check Proposal Status
function useProposalState(proposalId: bigint) {
return useReadContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'state',
args: [proposalId],
})
}
// State values:
// 0 = Pending
// 1 = Active
// 2 = Canceled
// 3 = Defeated
// 4 = Succeeded
// 5 = Queued
// 6 = Expired
// 7 = ExecutedDeployment Setup
After deploying PRIVGovernor and PRIVTimelock:
- Grant PROPOSER_ROLE to Governor:
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));- Grant CANCELLER_ROLE to Governor:
timelock.grantRole(timelock.CANCELLER_ROLE(), address(governor));- Grant EXECUTOR_ROLE to anyone (optional, but recommended):
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));- Renounce admin role (important for decentralization):
timelock.renounceRole(timelock.DEFAULT_ADMIN_ROLE(), deployer);- Transfer contract ownership to Timelock:
// For each protocol contract:
dataXchange.transferOwnership(address(timelock));
adNetwork.transferOwnership(address(timelock));
// etc.Security Notes
-
Delegation Required: PRIV tokens do not automatically grant voting power. Token holders MUST delegate to themselves or another address to participate in governance.
-
Timelock Delay: The 2-day timelock delay gives the community time to react to passed proposals. Users can exit positions or raise concerns before execution.
-
Spam Protection: The 3 active proposal limit and 1-day cooldown prevent spam attacks where malicious actors flood governance with proposals.
-
Quorum Requirement: 4% of total supply must participate for a proposal to be valid. This prevents small minorities from making decisions.
-
Open Execution: Anyone can call execute() once the timelock delay passes. This prevents censorship of valid proposals.
-
Admin Renouncement: After setup, the timelock admin role should be renounced. All future changes must go through governance.