Smart Contracts
Technical reference for the Curyo smart contract architecture.
Architecture
The upgradeable control-plane contracts use UUPS proxies: ContentRegistry, RoundVotingEngine, RoundRewardDistributor, FrontendRegistry, and ProfileRegistry. Token, identity, faucet, participation, governance, and helper contracts are intentionally non-upgradeable.
The current production surface also includes one stateless helper contract, SubmissionCanonicalizer, plus the protocol libraries used by the registries and voting engine.
| Contract | Role | Upgradeable |
|---|---|---|
| CuryoReputation | ERC-20 token (cREP) with governance voting power and flash-loan protection | No |
| VoterIdNFT | Soulbound ERC-721 representing verified human identity (sybil resistance) | No |
| ContentRegistry | Content lifecycle: submission, dormancy, rating updates, slashing | UUPS |
| RoundVotingEngine | Core voting: tlock commit-reveal voting, epoch-weighted rewards, deterministic settlement | UUPS |
| RoundRewardDistributor | Pull-based reward claiming for settled rounds | UUPS |
| FrontendRegistry | Frontend operator registration and fee distribution | UUPS |
| CategoryRegistry | Category/platform management via governance proposals | No |
| ParticipationPool | Halving-tier participation rewards used by submitter and voter reward claims | No |
| ProfileRegistry | On-chain user profiles with unique names, images, and public rating strategy text | UUPS |
| HumanFaucet | Sybil-resistant token distribution via Self.xyz passport verification | No |
| SubmissionCanonicalizer | Stateless URL/domain canonicalization helper used by ContentRegistry submissions | No |
| CuryoGovernor | On-chain governance with timelock (proposals, voting, execution) | No |
| RoundLib | Library: round state management and settlement logic | — |
| RewardMath | Library: pool split (82/5/10/2/1) and reward calculations | — |
| CategoryFeeLib | Library: category-fee routing for settled rounds | — |
| SubmitterStakeLib | Library: submitter stake return/slash policy helpers | — |
| TokenTransferLib | Library: narrow token transfer helpers used by reward settlement paths | — |
CuryoReputation
ERC-20 token with ERC20Votes for governance, ERC20Permit for scoped approvals, and ERC-1363 transfer hooks for one-transaction voting. Fixed supply of 100M with 6 decimals.
Key Features
- Governance voting power: Delegates can vote on proposals via CuryoGovernor.
- Governance lock: Tokens become non-transferable for 7 days when proposing or voting on governance proposals. This is a transfer lock, not a per-proposal escrowed bond.
- Flash-loan protection: Tracks first-receive block to prevent same-block vote attacks.
- Minting: Only
MINTER_ROLE(HumanFaucet) can mint, up toMAX_SUPPLY. - Single-tx voting: The production UI now uses
transferAndCall()so cREP transfer and vote commit happen atomically in one wallet transaction.
Key Functions
mint(to, amount)— Mint tokens (MINTER_ROLE only).lockForGovernance(account, amount)— Lock tokens for 7 days (governor only).getTransferableBalance(account)— Returns balance minus locked amount.transferAndCall(votingEngine, amount, payload)— Default vote path used by the app. Sends cREP stake to the voting engine and atomically commits the encrypted vote payload.
VoterIdNFT
Soulbound (non-transferable) ERC-721 representing a verified human identity. Minted by HumanFaucet upon successful Self.xyz passport verification. Token ID 0 is reserved (indicates no Voter ID).
Sybil Resistance
VoterIdNFT is required by most contracts to perform actions: submitting content, voting, registering frontends, creating profiles, and submitting categories. It also enforces a per-Voter-ID stake cap of 100 cREP per content per round, preventing a single identity from dominating any vote.
Key Functions
mint(holder, nullifier)— Mint a new Voter ID (authorized minters only, e.g., HumanFaucet).revokeVoterId(holder)— Revoke a Voter ID (owner/governance).recordStake(contentId, roundId, tokenId, amount)— Record stake against a Voter ID (voting engine only).hasVoterId(address)/getTokenId(address)— Check identity status (resolves delegates transparently).
Delegation
VoterIdNFT supports delegation: an SBT holder (cold wallet) can authorize a delegate (hot wallet) to act on their behalf. The delegate transparently passes all Voter ID checks without holding an SBT. Setup and security guidance now live in the /settings?tab=delegation flow.
setDelegate(address)— Authorize a delegate (holder only).removeDelegate()— Revoke delegate authorization (holder only).resolveHolder(address)— Returns the effective SBT holder for an address.
ContentRegistry
Manages content lifecycle. Each item has a unique ID and content hash stored on-chain; full URL and metadata are emitted via events.
Submission canonicalization is delegated to SubmissionCanonicalizer, which normalizes supported platform URLs into a deterministic submission key before duplicate checks are applied.
| Status | Description |
|---|---|
| Active | Accepting votes. Default state after submission. |
| Dormant | No activity for 30 days. Can be revived up to 2 times (expires after 90 days). |
| Cancelled | Voluntarily removed by the submitter (1 cREP cancellation fee). |
Key Functions
submitContent(url, title, description, tags, categoryId)— Submit content (10 cREP stake). Requires Voter ID. Duplicate URLs are rejected, and the title plus description are emitted in the canonicalContentSubmittedevent for indexers and alternate frontends.cancelContent(contentId)— Cancel own content (1 cREP fee to the configured cancellation-fee sink, treasury by default).markDormant(contentId)— Mark inactive content as dormant after 30 days. Permissionless; reverts if content has an active open round.reviveContent(contentId)— Revive dormant content (5 cREP, max 2 times).updateRatingDirect(contentId, newRating)— Called by RoundVotingEngine after settlement with the new rating computed from the final revealed up and down stake pools using the protocol's smoothed stake-imbalance formula.
Submitter Stake
- Grace period: 24 hours. No slash possible during this time.
- Slash: If a settled round establishes rating below 25 after grace period, 100% of stake goes to the treasury.
- Auto-return: After ~4 days once a settled round confirms rating stays above 25 and no later round remains open. If no round ever settles, the stake resolves when the content reaches dormancy after all open rounds have been closed.
- Submitter participation reward: Healthy submitter rewards are snapshotted when the stake returns. If the ParticipationPool is temporarily depleted, the remaining amount stays claimable later instead of being lost.
RoundVotingEngine
Manages per-content voting rounds with tlock commit-reveal voting, epoch-weighted rewards, and deterministic settlement. One-sided rounds (consensus) receive a subsidy from the consensus subsidy reserve.
Configuration
| Parameter | Value | Description |
|---|---|---|
| MIN_STAKE | 1 cREP | Minimum vote stake |
| MAX_STAKE | 100 cREP | Maximum vote stake per Voter ID per round |
| epochDuration | 20 minutes | Duration of each reward tier |
| maxDuration | 7 days | Maximum round lifetime — expired rounds can be cancelled |
| minVoters | 3 | Minimum revealed votes required before settlement is allowed |
| maxVotersPerRound | 1,000 | Cap on voters per content per round (O(1) settlement) |
| revealGracePeriod | 1 hour | Time after each epoch during which all past-epoch votes must be revealed before settlement |
| VOTE_COOLDOWN | 24 hours | Time before the same effective voter ID can vote on the same content again |
Key Functions
CuryoReputation.transferAndCall(votingEngine, stakeAmount, abi.encode(contentId, commitHash, ciphertext, frontend))— Default one-transaction vote flow. Transfers cREP and records the tlock-encrypted commit atomically. Direction is hidden until the epoch ends. Requires Voter ID and enforces the same 1–100 cREP stake bounds.commitVote(...)— Lower-level integration path for bots, tests, and direct contract callers that prefer explicit approvals instead of the default single-transaction transfer-and-call flow.revealVoteByCommitKey(contentId, roundId, commitKey, isUp, salt)— Reveal a previously committed vote after the epoch ends. Normally called by the keeper after off-chain drand/tlock decryption, but any caller that knows the plaintext(isUp, salt)can submit it. The production UI keeps this mostly hidden, but connected users also have a small manual fallback link if an auto-reveal appears delayed. The chain binds the reveal to the exact submitted ciphertext viakeccak256(ciphertext), but it still does not prove on-chain that the ciphertext was honestly decryptable. A future hardening path here would be zk-based reveal proofs.settleRound(contentId, roundId)— Settle the current round once at leastminVotersvotes are revealed and all past-epoch votes have been revealed (or their 1 hour reveal grace period has expired). Determines winners based on epoch-weighted stakes, splits reward pools, and updates content rating.RoundRewardDistributor.claimFrontendFee(contentId, roundId, frontend)— Frontend operators claim their proportional share of the 3% frontend fee pool. Pull-based, permissionless. Historical fee shares still follow the commit-time approval snapshot, but if the frontend is slashed or underbonded at claim time, the claim is redirected to the protocol instead of accruing to the operator.RoundRewardDistributor.claimParticipationReward(contentId, roundId)— Voters claim participation rewards (rate snapshotted at settlement time for fairness). Pull-based.ContentRegistry.claimSubmitterParticipationReward(contentId)— Claim the snapshotted submitter participation reward after a healthy stake return. Any amount the pool could already fund is reserved up front for that claim instead of depending entirely on future pool authorization state.cancelExpiredRound(contentId, roundId)— Cancel a round that exceeded maxDuration (7 days) without reaching commit quorum (minVoterstotal commits). Refundable to participants.finalizeRevealFailedRound(contentId, roundId)— Finalize a round that reached commit quorum, but still failed to reach reveal quorum after voting closed and the final reveal grace deadline passed.claimCancelledRoundRefund(contentId, roundId)— Claim refund for a cancelled, tied, or reveal-failed round.
RoundRewardDistributor
Pull-based reward claiming. Not pausable — users can always withdraw their tokens.
claimReward(contentId, roundId)— Claim settled-round voter payouts. Winners receive stake plus winnings; revealed losers receive a fixed 5% rebate.claimSubmitterReward(contentId, roundId)— Claim submitter's 10% share.sweepStrandedCrepToTreasury()— Governance-only recovery path for any cREP mistakenly sent directly to the distributor.
FrontendRegistry
Manages frontend operator registration and fee distribution. Frontend operators stake a fixed 1,000 cREP and receive 3% of the remaining 95% for each settled two-sided round they facilitated votes in.
Key Functions
register()— Register as frontend operator (fixed 1,000 cREP stake). Requires Voter ID.requestDeregister()/completeDeregister()— Start voluntary exit, then withdraw stake + pending fees after the unbonding window elapses.topUpStake(amount)— Restore the fixed 1,000 cREP bond after a partial slash so governance can approve the frontend again.approveFrontend(address)/revokeFrontend(address)— Governance controls approval. Approval requires the full bond to be restored.claimFees()— Claim accumulated platform fees while healthy and fully bonded.slashFrontend(address, amount, reason)— Slash frontend stake (governance). Any already accrued frontend fees are confiscated to the protocol at the same time.
CategoryRegistry
Manages content categories. New categories require a governance proposal and on-chain vote for approval. Each category maps to a domain and includes subcategories that help voters interpret the type of content being rated.
Key Functions
submitCategory(name, domain, subcategories)— Submit category for governance sponsorship (100 cREP stake). Requires Voter ID.linkApprovalProposal(categoryId, descriptionHash)— Link the separately created governor approval proposal to the pending category. Submitter only.clearApprovalProposal(categoryId)— Clear a linked approval proposal after it was canceled or expired so the submitter can retry or cancel.cancelUnlinkedCategory(categoryId)— Reclaim stake after 7 days if no approval proposal was linked.approveCategory(categoryId)— Approve after successful governance vote (timelock only).rejectCategory(categoryId)— Reject after a defeated vote (permissionless, checks proposal state).addApprovedCategory(name, domain, subcategories)— Add category directly (ADMIN_ROLE, for bootstrapping).
ProfileRegistry
On-chain user profiles with unique names (3–20 characters) and an optional public rating strategy. Profile settings also support an on-chain generated avatar color override. Requires Voter ID.
Key Functions
setProfile(name, strategy)— Create or update profile. Names are case-insensitive unique, andstrategystores a short public note about how the user rates on Curyo.getProfile(address)— Get profile (name, strategy, createdAt, updatedAt).getAddressByName(name)— Reverse lookup: name to owner address.setAvatarAccent(rgb)andclearAvatarAccent()— Set or remove the generated avatar color override.getAvatarAccent(address)— Read whether an avatar color override is set and the stored RGB value.
HumanFaucet
Sybil-resistant token distribution using Self.xyz zero-knowledge passport verification. Five tiers from Genesis (10,000 cREP for the first 10 users) down to Settler (1 cREP), with each tier doubling in size while the claim halves. Referral bonuses are 50% of the claim amount for both claimant and referrer.
On a successful claim, HumanFaucet also mints a VoterIdNFT for the claimant, enabling participation across the platform.
CuryoGovernor
OpenZeppelin Governor with timelock control. Uses cREP voting power (ERC20Votes). Tokens are locked for 7 days when proposing or casting votes.
| Parameter | Value |
|---|---|
| Voting delay | ~1 day (7,200 blocks) |
| Voting period | ~1 week (50,400 blocks) |
| Proposal threshold | 100 cREP |
| Quorum | 4% of circulating supply (min 10,000 cREP) |
| Governance lock | 7 days transfer-locked (when proposing or voting) |
ParticipationPool
Distributes participation rewards to both voters and content submitters. Voter rewards are claimed after round settlement using the rate snapshotted at settlement time. Submitter rewards are snapshotted only when a healthy submitter stake return resolves after a settled round. Funded with 34M cREP. Uses a halving schedule: starting at 90% reward rate, halving each time a tier threshold is reached (2M, 6M, 14M, 30M cumulative), with a 1% floor rate.
Libraries
RewardMath
splitPoolAfterLoserRefund(losingPool)— Reserve a 5% rebate for revealed losers, then split the remaining pool into 80% voters / 5% consensus subsidy / 10% submitter / 4% platform (3% frontend + 1% category) / 1% treasury.calculateVoterReward(shares, totalWinningShares, voterPool)— Share-proportional reward from the content-specific pool. 100% of the voter share goes to the content-specific pool.calculateRating(totalUpStake, totalDownStake)— Returns the final 0–100 rating using the protocol's smoothed stake-imbalance formula with a fixed 50 cREP parameter.
RoundLib
Helpers for round state management: tracks round lifecycle (Open, Settled, Cancelled, Tied, RevealFailed) and settlement logic.
Security
- UUPS Upgradeable: Core registries and voting contracts are upgradeable via UPGRADER_ROLE (governance timelock).
- Reentrancy Guard: All token-transferring functions use ReentrancyGuard.
- Flash-Loan Protection: CuryoReputation tracks first-receive block to prevent same-block vote attacks.
- Sybil Resistance: VoterIdNFT (soulbound) required for all user actions. Per-identity stake cap of 100 cREP per content per round.
- Governance Lock: Tokens are transfer-locked for 7 days when proposing or voting on governance. Proposal eligibility is checked from the prior voting-power snapshot, so the threshold is not a per-proposal bond and the same voting power can support multiple concurrent proposals.
- Pausable: ContentRegistry, RoundVotingEngine, and HumanFaucet can be paused. RoundRewardDistributor cannot be paused (users can always withdraw).
- Governance-First Access Control: The governance timelock holds DEFAULT_ADMIN_ROLE from deployment. The deployer receives only temporary setup roles (CONFIG_ROLE, MINTER_ROLE) with no ability to grant or escalate privileges. Ownable contracts (VoterIdNFT, HumanFaucet) restrict ownership transfer to the immutable governance address.