PRIV ProtocolPRIV Docs
Contracts

ApproverRegistry

Decentralized quality assurance through a stake-based approver system with multi-voter consensus and slashing.

Overview

ApproverRegistry is a game-theory QA system for data bounty submissions on the PRIV Protocol. Approvers stake PRIV to participate in multi-voter consensus reviews. Honest approvers earn per-vote rewards, while those who consistently vote against consensus accumulate dispute marks and face slashing. Slashed stakes flow back into the reward pool, making the system self-sustaining.

The contract integrates directly with BountyEscrow: when a review reaches consensus approval, ApproverRegistry forwards the result to BountyEscrow to trigger contributor payouts.

PropertyValue
ContractApproverRegistry
Address (Base Sepolia)0xB1536f831242bCa3357458dfCD6157e3F8e16227
Minimum Approver Stake500 PRIV
Quorum3 votes
Consensus Threshold66.67% (6,667 BPS)
Epoch Duration30 days
Max Disputes per Epoch5
Slash Percentage10% of stake
Review Timeout7 days
Max Voters per Review10
Reward per Correct Vote1 PRIV (configurable)
Standard Lock Duration30 days
Elite Lock Duration1 day
NetworkBase (Chain ID: 8453)

How It Works

Stake 500 PRIV  -->  Become approver  -->  Vote on submissions  -->  Quorum reached
      |                    |                       |                       |
      v                    v                       v                       v
  Lock 30 days      Active in registry     approve/reject + hash    Consensus resolved
                                                                          |
                                           +------------------------------+
                                           |                              |
                                     Correct vote                  Incorrect vote
                                           |                              |
                                           v                              v
                                   Earn reward tokens            Dispute mark added
                                                                          |
                                                                5 disputes/epoch?
                                                                    |         |
                                                                   Yes        No
                                                                    |         |
                                                                    v         v
                                                           10% slash +    No penalty
                                                           rewards denied  (resets next epoch)

Incentive Alignment

  • Honest voting: Earn per-vote rewards from the reward pool and keep your stake intact.
  • Occasional minority vote: No penalty. Dispute marks reset at the start of each new epoch.
  • Pattern of incorrect votes: Accumulating 5 or more disputes in a single epoch triggers a 10% stake slash, forfeiture of unclaimed rewards for that epoch, and potential deactivation if the remaining stake falls below 500 PRIV.
  • Sybil resistance: The 500 PRIV staking requirement makes it economically expensive to operate multiple malicious approver identities.

Registration

Becoming an Approver

To participate in the QA system, a user stakes at least 500 PRIV via registerApprover(). This transfers PRIV from the caller into the contract and marks the caller as an active approver.

function registerApprover(uint256 amount) external nonReentrant whenNotPaused
ParameterTypeDescription
amountuint256Amount of PRIV to stake (must be >= 500 PRIV)

Requirements:

  • Amount must be at least MIN_APPROVER_STAKE (500 PRIV)
  • Caller must not already be a registered active approver
  • Caller must have approved the contract to spend PRIV
  • Contract must not be paused

Topping Up Stake

If an approver's stake has been reduced by slashing, they can restore it via topUpStake():

function topUpStake(uint256 amount) external nonReentrant whenNotPaused

Unregistering

Approvers can exit the system and reclaim their stake plus any unclaimed rewards via unregisterApprover():

function unregisterApprover() external nonReentrant

Requirements:

  • Caller must be an active approver
  • Lock period must have elapsed (30 days standard, 1 day for elite approvers)

Voting and Consensus

Review Lifecycle

  1. An authorized reporter (typically the API backend) submits a bounty submission for review via submitForReview().
  2. Active approvers cast votes (approve or reject) via castVote().
  3. When the vote count reaches the quorum of 3, the review is automatically resolved.
  4. If a review does not reach quorum within 7 days, anyone can call resolveExpiredReview() to finalize it.

Casting a Vote

function castVote(bytes32 reviewId, bool approve) external whenNotPaused
ParameterTypeDescription
reviewIdbytes32The review to vote on
approvebooltrue to approve, false to reject

Requirements:

  • Caller must be an active approver with at least 500 PRIV staked
  • Caller must not have already voted on this review
  • Caller must not be the contributor whose submission is under review (self-review blocked)
  • Review must not be expired (7-day timeout) or already resolved
  • If the bounty requires a specific PrivBadge, the approver must hold that badge

Consensus Resolution

When the total votes on a review reach the quorum of 3, the contract determines the outcome:

  • Approved: At least 66.67% of votes are approve votes. The submission is forwarded to BountyEscrow for payout.
  • Rejected: Less than 66.67% approval. The submission is rejected and the contributor's pending payouts may be slashed.

After resolution, each voter is evaluated:

  • Correct voters (voted with consensus): correctVotes incremented, reward tokens credited to unclaimed balance.
  • Incorrect voters (voted against consensus): A dispute mark is added for the current epoch.

Slashing Mechanics

Dispute Accumulation

Each 30-day epoch, every approver starts with zero dispute marks. Each time an approver votes against the consensus outcome, one dispute mark is recorded. Dispute counts reset automatically at the start of each new epoch.

Slash Trigger

When an approver accumulates 5 or more disputes within a single epoch:

  1. 10% of their stake is slashed and transferred to the reward pool.
  2. All unclaimed rewards are forfeited for the current epoch.
  3. If the remaining stake drops below 500 PRIV, the approver is automatically deactivated.
Slash amount = stakedAmount * 1000 / 10000  (10%)

Slashed approvers cannot claim rewards until the next epoch begins and their dispute count resets.


Epoch Mechanics

Epochs are 30-day periods that govern dispute tracking and slashing resets.

Advancing Epochs

Anyone can advance to the next epoch once the current epoch's duration has elapsed:

function advanceEpoch() external

When a new epoch begins:

  • Dispute counters for all approvers are lazily reset (on next interaction)
  • The slashedThisEpoch flag is cleared
  • Slashed approvers regain the ability to claim rewards (if re-staked above minimum)

Accuracy Tracking

The contract tracks each approver's lifetime voting accuracy:

  • totalVotes: Total votes cast across all reviews
  • correctVotes: Votes that aligned with the final consensus

Accuracy Calculation

accuracyBPS = (correctVotes * 10000) / totalVotes

Returned in basis points (e.g., 9500 = 95% accuracy).

Elite Approver Status

Approvers who meet both criteria receive a reduced lock duration of 1 day (instead of 30 days) when unregistering:

RequirementThreshold
Minimum total votes10,000
Minimum accuracy98% (9,800 BPS)
function isEliteApprover(address _approver) external view returns (bool)
function getApproverLockDuration(address _approver) external view returns (uint256)

Submitter Reputation

The contract also tracks contributor (submitter) reputation across reviews:

  • successfulSubmissions: Incremented when a review resolves as approved
  • failedSubmissions: Incremented when a review resolves as rejected

This reputation feeds into getPayoutDelay(), which calculates a reduced timelock for contributors with strong track records:

  • Base delay: 30 days
  • Reputation reduction: 1 day off per net successful submission (max 15 days)
  • Staking reduction: Up to 15 days off for contributors with 1,000+ effective PRIV staked in PRIVStakingV2

The minimum possible delay is 0 days for contributors with both excellent reputation and significant staking positions.


Rewards

Earning Rewards

Each correct vote credits the approver with rewardPerCorrectVote PRIV (default: 1 PRIV, configurable by owner). Rewards accumulate in the unclaimedRewards mapping.

Claiming Rewards

function claimRewards() external nonReentrant

Requirements:

  • Caller must be an active approver
  • Must have unclaimed rewards greater than 0
  • Must not be slashed in the current epoch

Claims are capped to the available reward pool balance.

Reward Pool

The reward pool is funded from two sources:

  1. Direct deposits: Anyone can deposit PRIV via depositRewards().
  2. Slashed stakes: When an approver is slashed, the slashed amount flows directly into the reward pool.

Contract Interface

Constants

uint256 public constant MIN_APPROVER_STAKE = 500e18;        // 500 PRIV
uint256 public constant QUORUM = 3;                          // Votes needed
uint256 public constant CONSENSUS_BPS = 6667;                // 66.67%
uint256 public constant BPS_DENOMINATOR = 10_000;
uint256 public constant EPOCH_DURATION = 30 days;
uint256 public constant SLASH_BPS = 1000;                    // 10%
uint256 public constant MAX_DISPUTES_PER_EPOCH = 5;
uint256 public constant APPROVER_LOCK_DURATION = 30 days;
uint256 public constant ELITE_LOCK_DURATION = 1 days;
uint256 public constant ELITE_ACCURACY_BPS = 9800;           // 98%
uint256 public constant ELITE_MIN_VOTES = 10_000;
uint256 public constant REVIEW_TIMEOUT = 7 days;
uint256 public constant MAX_VOTERS_PER_REVIEW = 10;
uint256 public constant BASE_PAYOUT_DELAY = 30 days;

Structs

Approver

struct Approver {
    uint256 stakedAmount;
    uint256 stakedAt;
    uint256 totalVotes;
    uint256 correctVotes;
    uint256 currentEpochDisputes;
    uint256 lastDisputeResetEpoch;
    bool slashedThisEpoch;
    bool active;
}

Review

struct Review {
    uint256 bountyId;
    address contributor;
    bytes32 submissionHash;
    uint256 approveVotes;
    uint256 rejectVotes;
    uint256 createdAt;
    bool resolved;
    bool outcome;        // true = approved, false = rejected
}

View Functions

function getApproverAccuracy(address _approver) external view returns (uint256)
function getEpochDisputes(address _approver) external view returns (uint256)
function isSlashedThisEpoch(address _approver) external view returns (bool)
function getReviewVoterCount(bytes32 reviewId) external view returns (uint256)
function getReviewVoter(bytes32 reviewId, uint256 index) external view returns (address)
function getApproverLockDuration(address _approver) external view returns (uint256)
function isEliteApprover(address _approver) external view returns (bool)
function getPayoutDelay(address contributor) external view returns (uint256)

Admin Functions

function addReporter(address reporter) external onlyOwner
function removeReporter(address reporter) external onlyOwner
function setRewardPerCorrectVote(uint256 _reward) external onlyOwner
function setPrivBadges(address _privBadges) external onlyOwner
function setPrivStaking(address _privStaking) external onlyOwner
function pause() external onlyOwner
function unpause() external onlyOwner

Events

event ApproverRegistered(address indexed approver, uint256 stake);
event ApproverUnregistered(address indexed approver, uint256 stakeReturned);
event ApproverSlashed(address indexed approver, uint256 slashAmount, uint256 epochDisputes);
event ApproverTopUp(address indexed approver, uint256 amount);

event ReviewCreated(bytes32 indexed reviewId, uint256 bountyId, address contributor, bytes32 submissionHash);
event VoteCast(bytes32 indexed reviewId, address indexed voter, bool approved);
event ReviewResolved(bytes32 indexed reviewId, bool approved, uint256 approveVotes, uint256 rejectVotes);

event RewardsClaimed(address indexed approver, uint256 amount);
event RewardsDeposited(uint256 amount);
event RewardPerVoteUpdated(uint256 newReward);

event ReporterAdded(address indexed reporter);
event ReporterRemoved(address indexed reporter);
event EpochAdvanced(uint256 indexed epoch);

Errors

error ZeroAddress();
error ZeroAmount();
error InsufficientStake();
error AlreadyRegistered();
error NotRegistered();
error NotActive();
error AlreadyVoted();
error ReviewAlreadyResolved();
error ReviewNotFound();
error ReviewExpired();
error CooldownNotMet();
error NoRewardsToClaim();
error SlashedThisEpoch();
error NotReporter();
error SelfReview();
error StakeStillLocked();
error ActiveReviewsPending();
error EpochNotEnded();

Usage Examples

Register as an Approver

import { useWriteContract } from 'wagmi'
import { parseEther } from 'viem'

function useRegisterApprover() {
  const { writeContract } = useWriteContract()

  return async (amount: string) => {
    const amountWei = parseEther(amount)

    // 1. Approve the ApproverRegistry contract
    await writeContract({
      address: PRIV_TOKEN_ADDRESS,
      abi: privTokenAbi,
      functionName: 'approve',
      args: [APPROVER_REGISTRY_ADDRESS, amountWei],
    })

    // 2. Register with at least 500 PRIV
    await writeContract({
      address: APPROVER_REGISTRY_ADDRESS,
      abi: approverRegistryAbi,
      functionName: 'registerApprover',
      args: [amountWei],
    })
  }
}

Cast a Vote on a Review

function useCastVote() {
  const { writeContract } = useWriteContract()

  return (reviewId: `0x${string}`, approve: boolean) => {
    writeContract({
      address: APPROVER_REGISTRY_ADDRESS,
      abi: approverRegistryAbi,
      functionName: 'castVote',
      args: [reviewId, approve],
    })
  }
}

Check Approver Accuracy

import { useReadContract } from 'wagmi'

function useApproverAccuracy(address: string) {
  const { data: accuracyBps } = useReadContract({
    address: APPROVER_REGISTRY_ADDRESS,
    abi: approverRegistryAbi,
    functionName: 'getApproverAccuracy',
    args: [address],
  })

  // Convert basis points to percentage
  const accuracyPercent = accuracyBps ? Number(accuracyBps) / 100 : 0
  return accuracyPercent
}

Claim Rewards

function useClaimApproverRewards() {
  const { writeContract } = useWriteContract()

  return () => {
    writeContract({
      address: APPROVER_REGISTRY_ADDRESS,
      abi: approverRegistryAbi,
      functionName: 'claimRewards',
    })
  }
}

Security Notes

  1. Reentrancy Protection: All functions involving token transfers use the nonReentrant modifier.

  2. Self-Review Prevention: Contributors cannot vote on their own submissions, preventing approval fraud.

  3. Review Timeout: Reviews that do not reach quorum within 7 days can be resolved by anyone. Reviews with zero votes are auto-rejected; reviews with partial votes are resolved using the existing vote distribution.

  4. Badge Gating: If a bounty specifies a required PrivBadge, approvers must hold that badge to vote, ensuring domain expertise.

  5. Oracle Integration: ApproverRegistry acts as a trusted oracle for BountyEscrow, forwarding approved submissions and managing heartbeat pings.

  6. Emergency Pause: The owner can pause the contract in emergencies, halting registrations, reviews, and voting.


Source Code

View on GitHub