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.

ContractRoleUpgradeable
HumanReputationERC-20 token (HREP) with governance voting power, ERC-1363 hooks, and governance locksNo
VoterIdNFTSoulbound ERC-721 representing verified human identity (sybil resistance)No
ContentRegistryContent lifecycle: submission, dormancy, rating updates, slashingTransparent
ProtocolConfigGovernance-controlled address book and round configuration for RoundVotingEngineTransparent
RoundVotingEngineCore voting: tlock commit-reveal voting, epoch-weighted rewards, deterministic settlementTransparent
RoundRewardDistributorPull-based reward claiming for settled roundsTransparent
FrontendRegistryFrontend operator registration and fee distributionTransparent
CategoryRegistrySeeded discovery category metadataNo
ParticipationPoolHalving-tier HREP Bootstrap Pool rewards used by voter reward claimsNo
QuestionRewardPoolEscrowQuestion-scoped HREP or USDC custody, voter rewards, and the frontend-operator reward shareNo
FeedbackBonusEscrowQuestion-scoped USDC bonuses for awarded voter feedback hashesTransparent
ProfileRegistryOn-chain user profiles with unique names, images, and public self-reported audience contextTransparent
HumanFaucetSybil-resistant token distribution via Self.xyz age, document, and sanctions verificationNo
CuryoGovernorOn-chain governance with timelock (proposals, voting, execution)No
RoundLibLibrary: round state management and settlement logic
RewardMathLibrary: pool split (90/5/4/1) and reward calculations
TokenTransferLibLibrary: 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 to MAX_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.

StatusDescription
ActiveAccepting votes. Default state after submission.
DormantNo 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.
CancelledVoluntarily removed by the submitter (1 HREP cancellation fee).

Key Functions

  • reserveSubmission(revealCommitment), then submitQuestionWithRewardAndRoundConfig(..., 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: questionMetadataHash and resultSpecHash. 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 canonical ContentSubmitted event for indexers and alternate frontends. The subjective template, rationale, and interpretation data stays off-chain; the contract only commits to its hashes and emits QuestionSpecAnchored. 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. requiredSettledRounds now 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

ParameterValueDescription
MIN_STAKE1 HREPMinimum vote stake
MAX_STAKE100 HREPMaximum vote stake per Voter ID per round
epochDuration20 minutesDefault duration of each reward tier; question creators can select within governance bounds.
maxDuration7 daysDefault maximum round lifetime; question creators can select within governance bounds.
minVoters3Default minimum revealed votes required before settlement is allowed.
maxVotersPerRound200Default cap on voters per content per round and upper bound for bounty voter requirements.
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

  • 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, and drandChainHash so indexers can observe the exact reveal metadata attached to each vote. The redeployed engine also snapshots roundReferenceRatingBps and emits RoundConfigSnapshotted per 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 via keccak256(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 least minVoters votes 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 (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.

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(...) and validateRoundConfig(...) — 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(...), and setTreasury(...) — 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, and selfReport stores public, self-reported, unverified audience context.
  • getProfile(address) — Get profile (name, selfReport, 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 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.

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