Smart Contracts
Technical reference for the Curyo smart contract architecture.
Architecture
The upgradeable control-plane contracts use transparent proxies managed by timelock-owned proxy admins: ContentRegistry, ProtocolConfig, RoundVotingEngine, RoundRewardDistributor, FrontendRegistry, and ProfileRegistry. Token, identity, faucet, participation, governance, and helper contracts are intentionally non-upgradeable.
The Solidity sources live in packages/foundry/contracts, deployment artifacts live in packages/foundry/deployments, and the shared TypeScript ABIs and address helpers used by the app and SDK live in packages/contracts.
| Contract | Role | Upgradeable |
|---|---|---|
| HumanReputation | ERC-20 token (HREP) with governance voting power, ERC-1363 hooks, and governance locks | No |
| VoterIdNFT | Soulbound ERC-721 representing verified human identity (sybil resistance) | No |
| ContentRegistry | Content lifecycle: submission, dormancy, rating updates, slashing | Transparent |
| ProtocolConfig | Governance-controlled address book and round configuration for RoundVotingEngine | Transparent |
| RoundVotingEngine | Core voting: tlock commit-reveal voting, epoch-weighted rewards, deterministic settlement | Transparent |
| RoundRewardDistributor | Pull-based reward claiming for settled rounds | Transparent |
| FrontendRegistry | Frontend operator registration and fee distribution | Transparent |
| CategoryRegistry | Seeded discovery category metadata | No |
| ParticipationPool | Halving-tier HREP Bootstrap Pool rewards used by voter reward claims | No |
| QuestionRewardPoolEscrow | Question-scoped HREP or USDC custody, voter rewards, and the frontend-operator reward share | No |
| FeedbackBonusEscrow | Question-scoped USDC bonuses for awarded voter feedback hashes | Transparent |
| ProfileRegistry | On-chain user profiles with unique names, images, and public self-reported audience context | Transparent |
| HumanFaucet | Sybil-resistant token distribution via Self.xyz age, document, and sanctions verification | No |
| CuryoGovernor | On-chain governance with timelock (proposals, voting, execution) | No |
| RoundLib | Library: round state management and settlement logic | — |
| RewardMath | Library: pool split (90/5/4/1) and reward calculations | — |
| TokenTransferLib | Library: narrow token transfer helpers used by reward settlement paths | — |
HumanReputation
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.
- Snapshot-based governance: ERC20Votes provides historical voting-power snapshots for governance, while HREP transfer locks apply after proposing or voting.
- Minting: Only
MINTER_ROLE(HumanFaucet) can mint, up toMAX_SUPPLY. - Single-tx voting: The production UI now uses
transferAndCall()so HREP 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 HREP 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 or biometric ID verification for an eligible 18+ claimant. Token ID 0 is reserved (indicates no Voter ID).
Sybil Resistance
VoterIdNFT is required for voting, registering frontends, creating profiles, and creating categories. USDC-funded question submission is permissionless and does not require a Voter ID; HREP-funded identity paths stay gated where the contracts require them. VoterIdNFT also enforces a per-Voter-ID stake cap of 100 HREP 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 for flows that accept delegated identities, notably content submission and voting. Holder-only actions such as frontend registration, profile management, and category submission still require the SBT holder address itself. 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.
ContentRegistry validates submitted media links against CategoryRegistry before deriving the question submission key from the submitted metadata. The docs now describe the question-first flow: a required context URL with optional image or YouTube preview media, plus a mandatory non-refundable bounty attached at submission in HREP or USDC.
| Status | Description |
|---|---|
| Active | Accepting votes. Default state after submission. |
| Dormant | No meaningful activity for 30 days. The original submitter can revive it up to 2 times during the 1-day exclusive revival window before the dormant key becomes releasable. |
| Cancelled | Voluntarily removed by the submitter (1 HREP cancellation fee). |
Key Functions
reserveSubmission(revealCommitment), thensubmitQuestionWithRewardAndRoundConfig(..., rewardTerms, roundConfig, spec)— Reserve a hidden question, then reveal it with the exact attached bounty terms, creator-selected round config, and two non-zero metadata hashes:questionMetadataHashandresultSpecHash. Question text is capped at 120 characters, the context/media submission key is checked for duplicates, and the question plus description are emitted in the canonicalContentSubmittedevent for indexers and alternate frontends. The subjective template, rationale, and interpretation data stays off-chain; the contract only commits to its hashes and emitsQuestionSpecAnchored. Agent asks use the same function after the user or scoped agent wallet executes the returned funding and submission calls.submitQuestionBundleWithRewardAndRoundConfig(..., rewardTerms, roundConfig)— Submit a ranked-option bundle with one bounty shared across sibling questions.requiredSettledRoundsnow applies to bundle round sets, where each set is complete only after every bundled question has one settled round.getContentRoundConfig(contentId)— Returns the blind phase, maximum duration, settlement voters, and voter cap selected for that question. Existing submit functions without an explicit round config still use the governed default.cancelContent(contentId)— Cancel own content (1 HREP 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 HREP, max 2 times). Only the original submitter identity can do this, and only during the 1-day exclusive revival window.updateRatingState(contentId, roundId, referenceRatingBps, nextState)— Called by RoundVotingEngine after settlement with the score-relative update derived from the round's snapshotted reference score, epoch-weighted revealed evidence, and conservative rating bound.
Submission Economics
Question submissions no longer carry refundable creator deposits or creator-side bootstrap rewards. The attached bounty is non-refundable and routes to eligible voters plus the eligible frontend operator.
RoundVotingEngine
Manages per-content voting rounds with tlock commit-reveal voting, explicit drand metadata binding, 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 HREP | Minimum vote stake |
| MAX_STAKE | 100 HREP | Maximum vote stake per Voter ID per round |
| epochDuration | 20 minutes | Default duration of each reward tier; question creators can select within governance bounds. |
| maxDuration | 7 days | Default maximum round lifetime; question creators can select within governance bounds. |
| minVoters | 3 | Default minimum revealed votes required before settlement is allowed. |
| maxVotersPerRound | 200 | Default cap on voters per content per round and upper bound for bounty voter requirements. |
| 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
HumanReputation.transferAndCall(votingEngine, stakeAmount, abi.encode(contentId, roundReferenceRatingBps, commitHash, ciphertext, frontend, targetRound, drandChainHash))— Default one-transaction vote flow. Transfers HREP and records the tlock-encrypted commit atomically. Direction is hidden until the epoch ends. Requires Voter ID and enforces the same 1–100 HREP stake bounds. The redeployed contract rejects malformed or non-armored ciphertexts, binds the canonical round reference score into the vote payload, and binds the reveal-target metadata on-chain.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.- VoteCommitted event: emits the commit hash,
targetRound, anddrandChainHashso indexers can observe the exact reveal metadata attached to each vote. The redeployed engine also snapshotsroundReferenceRatingBpsand emitsRoundConfigSnapshottedper round so every frontend can recover the exact score anchor and round settings users voted against. revealVoteByCommitKey(contentId, roundId, commitKey, isUp, salt)— Reveal a previously committed vote after the epoch ends. This remains the keeper-assisted/self-reveal path: the keeper normally performs off-chain drand/tlock decryption after validating the stored stanza metadata and submits the reveal, 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)and now rejects malformed/non-armored commits on-chain, 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 from the round snapshot 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 bounties, and updates content rating from the round reference score using the governed score-relative rating model.RoundRewardDistributor.claimFrontendFee(contentId, roundId, frontend)— Frontend operators claim their proportional share of the 3% frontend fee pool. Pull-based and operator-only. Historical fee shares still follow the commit-time eligibility snapshot, but if the frontend is slashed or underbonded at claim time, governance can route the claim to the protocol instead of accruing it to the operator.QuestionRewardPoolEscrow.claimQuestionReward(rewardPoolId, roundId)— Claim the USDC-backed bounty for a revealed voter. New bounties default to a 3% frontend-operator share, attributed from the vote commit; unpayable frontend shares remain with the voter claim.QuestionRewardPoolEscrow.claimQuestionBundleReward(bundleId, roundSetIndex)— Claim a bundle bounty round set after the voter revealed on every bundled question in that set. Multi-round bundles create one claimable allocation per completed round set.FeedbackBonusEscrow.awardFeedbackBonus(poolId, recipient, feedbackHash, grossAmount)— Pay an awarded feedback hash directly to a revealed, independent voter. The awarder pays this transaction, the recipient receives USDC immediately, and an eligible vote-attributed frontend receives the 3% share.FeedbackBonusEscrow.forfeitExpiredFeedbackBonus(poolId)— Send expired unawarded Feedback Bonus USDC to treasury.RoundRewardDistributor.claimParticipationReward(contentId, roundId)— Voters claim bootstrap rewards (rate snapshotted at settlement time for fairness). Pull-based.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.
ProtocolConfig
Governance-controlled address book and parameter store for RoundVotingEngine. Governance sets the default round config and creator bounds; each question then stores its selected config, and the engine snapshots that config plus reveal grace at round creation so mid-round governance changes do not change an already open round.
setConfig(epochDuration, maxDuration, minVoters, maxVoters)— Update round parameters for future questions that use the default config.setRoundConfigBounds(...)andvalidateRoundConfig(...)— Define and enforce the allowed creator-selected range for blind phase, max duration, settlement voters, and voter cap.setRevealGracePeriod(seconds)— Update the grace period used for future round snapshots.setRewardDistributor(...),setFrontendRegistry(...),setCategoryRegistry(...),setVoterIdNFT(...),setParticipationPool(...), andsetTreasury(...)— Maintain the engine's governance-controlled address book.
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.claimParticipationReward(contentId, roundId)— Claim the HREP bootstrap reward for eligible winning revealed voters, using the rate snapshotted at settlement.sweepStrandedHrepToTreasury()— Governance-only recovery path for any HREP mistakenly sent directly to the distributor.
FrontendRegistry
Manages frontend operator registration and fee distribution. Frontend operators stake a fixed 1,000 HREP and receive 4% of the remaining 95% for each settled two-sided round they facilitated votes in.
Key Functions
register()— Register as frontend operator (fixed 1,000 HREP 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 HREP bond after a partial slash so the frontend becomes fee-eligible again.claimFees()— Claim accumulated platform fees while healthy, fully bonded, and not exiting.slashFrontend(address, amount, reason)— Slash frontend stake (governance). Any already accrued frontend fees are confiscated to the protocol at the same time.
CategoryRegistry
Stores simple seeded discovery categories. Categories are metadata used to help people find and interpret content; they do not require user staking or governance approval proposals.
Key Functions
addCategory(name, slug, subcategories)— Add seeded category metadata (ADMIN_ROLE).
ProfileRegistry
On-chain user profiles with unique names (3–20 characters) and optional public self-reported audience context. Profile settings also support an on-chain generated avatar color override. Requires Voter ID.
Key Functions
setProfile(name, selfReport)— Create or update profile. Names are case-insensitive unique, andselfReportstores public, self-reported, unverified audience context.getProfile(address)— Get profile (name, selfReport, 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 or biometric ID-card verification. Claims require a supported credential, proof that the claimant is 18 or older, OFAC sanctions clearance, and the configured sanctioned-country exclusion check, currently covering Cuba, Iran, North Korea, and Syria. Five tiers run from Genesis (10,000 HREP for the first 10 users) down to Settler (1 HREP), with claim sizes stepping down 10x at claimant thresholds 10 / 1,000 / 10,000 / 1,000,000. Referral bonuses are 50% of the claim amount for both claimant and referrer.
On a successful claim, HumanFaucet attempts to mint a VoterIdNFT for the claimant, enabling participation across the platform. Governance can retry the mint if the claim succeeds but the NFT mint fails.
Privileged sweeps of accounted faucet funds are disabled in the current launch hardening.
CuryoGovernor
OpenZeppelin Governor with timelock control. Uses HREP 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 | 1,000 HREP |
| Quorum | 4% of circulating supply (min 100,000 HREP) |
| Governance lock | 7 days transfer-locked (when proposing or voting) |
ParticipationPool
Implements the user-facing Bootstrap Pool for voters. Voter rewards are claimed after round settlement using the rate snapshotted at settlement time. Funded with 12M HREP. Uses a halving schedule: starting at 90% reward rate, halving each time a tier threshold is reached (1.5M, 4.5M, 10.5M, 22.5M cumulative), with a 1% floor rate.
Privileged sweeps of accounted bootstrap rewards are disabled; only reward accounting and surplus recovery move funds.
Libraries
RewardMath
splitPoolAfterLoserRefund(losingPool)— Reserve a 5% rebate for revealed losers, then split the remaining pool into 90% voters / 5% consensus subsidy / 4% frontend / 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)— Legacy deployments use this smoothed stake-imbalance helper. The planned redeploy replaces it with a dedicated score-relative rating math library that consumes the round reference score, epoch-weighted evidence, dynamic confidence, and conservative-bound logic.
RoundLib
Helpers for round state management: tracks round lifecycle (Open, Settled, Cancelled, Tied, RevealFailed) and settlement logic.
Security
- Transparent proxies: Core registries and voting contracts are upgradeable through timelock-owned proxy admins.
- Reentrancy protection: Core registry, voting, reward, frontend, category, and participation flows use reentrancy guards; HumanFaucet uses a dedicated claim lock.
- Snapshot-based governance: CuryoGovernor uses ERC20Votes snapshots for proposal voting power, and governance participation also applies a 7-day HREP transfer lock.
- Sybil Resistance: VoterIdNFT (soulbound) remains required for voting and other identity-gated actions. Per-identity stake cap of 100 HREP per content per round, plus question-first submission guardrails and claim gating. Question submission is the same for humans, bots, and delegated agents.
- 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-owned access control: The governor/timelock owns upgrade, config, and treasury roles from launch. The initial 32M treasury allocation also sits there, while the deployer receives only temporary setup roles and renounces them after deployment.