PRIV ProtocolPRIV Docs
Contracts

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

PropertyValue
Voting Delay1 day (~43,200 blocks)
Voting Period7 days (~302,400 blocks)
Proposal Threshold100,000 PRIV
Quorum4% of total supply
Proposal Cooldown1 day between proposals
Max Active Proposals3 per address

PRIVTimelock

PropertyValue
Default Min Delay2 days
Role: ProposerPRIVGovernor
Role: ExecutorOpen (address(0))
Role: CancellerPRIVGovernor

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 Proposal

PRIVGovernor

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:

  1. Proposal Cooldown: 1 day minimum between proposals from same address
  2. Active Proposal Limit: Maximum 3 active proposals per address
  3. 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)
ParameterTypeDescription
targetsaddress[]Contract addresses to call
valuesuint256[]ETH values for each call
calldatasbytes[]Encoded function calls
descriptionstringProposal 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 ValueMeaning
0Against
1For
2Abstain

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) external

Note: 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 start
  • Active - Voting in progress
  • Canceled - Proposal was canceled
  • Defeated - Voting ended, did not pass
  • Succeeded - Voting ended, passed
  • Queued - Queued in timelock
  • Expired - Timelock period expired
  • Executed - 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

RolePurposeDefault Holder
PROPOSER_ROLECan queue operationsPRIVGovernor
EXECUTOR_ROLECan execute operationsaddress(0) (anyone)
CANCELLER_ROLECan cancel operationsPRIVGovernor
DEFAULT_ADMIN_ROLECan grant/revoke rolesInitial 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 = Executed

Deployment Setup

After deploying PRIVGovernor and PRIVTimelock:

  1. Grant PROPOSER_ROLE to Governor:
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
  1. Grant CANCELLER_ROLE to Governor:
timelock.grantRole(timelock.CANCELLER_ROLE(), address(governor));
  1. Grant EXECUTOR_ROLE to anyone (optional, but recommended):
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
  1. Renounce admin role (important for decentralization):
timelock.renounceRole(timelock.DEFAULT_ADMIN_ROLE(), deployer);
  1. Transfer contract ownership to Timelock:
// For each protocol contract:
dataXchange.transferOwnership(address(timelock));
adNetwork.transferOwnership(address(timelock));
// etc.

Security Notes

  1. Delegation Required: PRIV tokens do not automatically grant voting power. Token holders MUST delegate to themselves or another address to participate in governance.

  2. 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.

  3. Spam Protection: The 3 active proposal limit and 1-day cooldown prevent spam attacks where malicious actors flood governance with proposals.

  4. Quorum Requirement: 4% of total supply must participate for a proposal to be valid. This prevents small minorities from making decisions.

  5. Open Execution: Anyone can call execute() once the timelock delay passes. This prevents censorship of valid proposals.

  6. Admin Renouncement: After setup, the timelock admin role should be renounced. All future changes must go through governance.


Source Code