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.

ContractRoleUpgradeable
CuryoReputationERC-20 token (cREP) with governance voting power and flash-loan protectionNo
VoterIdNFTSoulbound ERC-721 representing verified human identity (sybil resistance)No
ContentRegistryContent lifecycle: submission, dormancy, rating updates, slashingUUPS
RoundVotingEngineCore voting: tlock commit-reveal voting, epoch-weighted rewards, deterministic settlementUUPS
RoundRewardDistributorPull-based reward claiming for settled roundsUUPS
FrontendRegistryFrontend operator registration and fee distributionUUPS
CategoryRegistryCategory/platform management via governance proposalsNo
ParticipationPoolHalving-tier participation rewards used by submitter and voter reward claimsNo
ProfileRegistryOn-chain user profiles with unique names, images, and public rating strategy textUUPS
HumanFaucetSybil-resistant token distribution via Self.xyz passport verificationNo
SubmissionCanonicalizerStateless URL/domain canonicalization helper used by ContentRegistry submissionsNo
CuryoGovernorOn-chain governance with timelock (proposals, voting, execution)No
RoundLibLibrary: round state management and settlement logic
RewardMathLibrary: pool split (82/5/10/2/1) and reward calculations
CategoryFeeLibLibrary: category-fee routing for settled rounds
SubmitterStakeLibLibrary: submitter stake return/slash policy helpers
TokenTransferLibLibrary: 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 to MAX_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.

StatusDescription
ActiveAccepting votes. Default state after submission.
DormantNo activity for 30 days. Can be revived up to 2 times (expires after 90 days).
CancelledVoluntarily 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 canonical ContentSubmitted event 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

ParameterValueDescription
MIN_STAKE1 cREPMinimum vote stake
MAX_STAKE100 cREPMaximum vote stake per Voter ID per round
epochDuration20 minutesDuration of each reward tier
maxDuration7 daysMaximum round lifetime — expired rounds can be cancelled
minVoters3Minimum revealed votes required before settlement is allowed
maxVotersPerRound1,000Cap on voters per content per round (O(1) settlement)
revealGracePeriod1 hourTime after each epoch during which all past-epoch votes must be revealed before settlement
VOTE_COOLDOWN24 hoursTime 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 via keccak256(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 least minVoters votes 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 (minVoters total 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, and strategy stores 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) and clearAvatarAccent() — 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.

ParameterValue
Voting delay~1 day (7,200 blocks)
Voting period~1 week (50,400 blocks)
Proposal threshold100 cREP
Quorum4% of circulating supply (min 10,000 cREP)
Governance lock7 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.