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.
| Property | Value |
|---|---|
| Contract | ApproverRegistry |
| Address (Base Sepolia) | 0xB1536f831242bCa3357458dfCD6157e3F8e16227 |
| Minimum Approver Stake | 500 PRIV |
| Quorum | 3 votes |
| Consensus Threshold | 66.67% (6,667 BPS) |
| Epoch Duration | 30 days |
| Max Disputes per Epoch | 5 |
| Slash Percentage | 10% of stake |
| Review Timeout | 7 days |
| Max Voters per Review | 10 |
| Reward per Correct Vote | 1 PRIV (configurable) |
| Standard Lock Duration | 30 days |
| Elite Lock Duration | 1 day |
| Network | Base (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| Parameter | Type | Description |
|---|---|---|
amount | uint256 | Amount 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 whenNotPausedUnregistering
Approvers can exit the system and reclaim their stake plus any unclaimed rewards via unregisterApprover():
function unregisterApprover() external nonReentrantRequirements:
- Caller must be an active approver
- Lock period must have elapsed (30 days standard, 1 day for elite approvers)
Voting and Consensus
Review Lifecycle
- An authorized reporter (typically the API backend) submits a bounty submission for review via
submitForReview(). - Active approvers cast votes (approve or reject) via
castVote(). - When the vote count reaches the quorum of 3, the review is automatically resolved.
- 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| Parameter | Type | Description |
|---|---|---|
reviewId | bytes32 | The review to vote on |
approve | bool | true 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):
correctVotesincremented, 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:
- 10% of their stake is slashed and transferred to the reward pool.
- All unclaimed rewards are forfeited for the current epoch.
- 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() externalWhen a new epoch begins:
- Dispute counters for all approvers are lazily reset (on next interaction)
- The
slashedThisEpochflag 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 reviewscorrectVotes: Votes that aligned with the final consensus
Accuracy Calculation
accuracyBPS = (correctVotes * 10000) / totalVotesReturned 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:
| Requirement | Threshold |
|---|---|
| Minimum total votes | 10,000 |
| Minimum accuracy | 98% (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 approvedfailedSubmissions: 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 nonReentrantRequirements:
- 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:
- Direct deposits: Anyone can deposit PRIV via
depositRewards(). - 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 onlyOwnerEvents
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
-
Reentrancy Protection: All functions involving token transfers use the
nonReentrantmodifier. -
Self-Review Prevention: Contributors cannot vote on their own submissions, preventing approval fraud.
-
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.
-
Badge Gating: If a bounty specifies a required PrivBadge, approvers must hold that badge to vote, ensuring domain expertise.
-
Oracle Integration: ApproverRegistry acts as a trusted oracle for BountyEscrow, forwarding approved submissions and managing heartbeat pings.
-
Emergency Pause: The owner can pause the contract in emergencies, halting registrations, reviews, and voting.
Source Code
BountyEscrow
Data bounty escrow contract with three payment tiers, oracle-driven quality assurance, auto-payout on approval, timelocked payouts, and fee routing through FeeManager.
DataPipeline
Recurring fresh data pipelines for AI model retraining with prepaid batch delivery, oracle consensus, and priority staking.