Smart Contracts
Technical reference for the RateLoop 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, launch distribution, 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 |
|---|---|---|
| LoopReputation | ERC-20 token (LREP) with governance voting power, ERC-1363 hooks, and governance locks | No |
| VoterIdNFT | Soulbound ERC-721 representing optional verified-rater credentials | No |
| RaterRegistry | Optional human credentials, rater profiles, trust seeds, and cluster-score discounts | No |
| RaterDeclarationRegistry | Bonded AI model/operator declarations, probes, drift flags, challenges, and slashing | 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 | Optional governance-funded participation rewards used by voter reward claims | No |
| LaunchDistributionPool | 64M LREP launch rewards: 35M verified/referral, 25M anchor-gated earned rater, and 4M legacy users | No |
| QuestionRewardPoolEscrow | Question-scoped LREP 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 |
| CuryoGovernor | On-chain governance with timelock (proposals, voting, execution) | No |
| RoundLib | Library: round state management and settlement logic | — |
| RewardMath | Library: 5% revealed-loser rebate, 91/3/1/5 remaining-pool split, and reward calculations | — |
| TokenTransferLib | Library: narrow token transfer helpers used by reward settlement paths | — |
LoopReputation
ERC-20 token with ERC20Votes for governance, ERC20Permit for scoped approvals, and a capped reputation supply. The rating flow supports explicit LREP approvals into the voting engine when a rater chooses to stake.
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 LREP transfer locks apply after proposing or voting.
- Supply cap: Distribution and reward recycling stay bounded by
MAX_SUPPLY. - RBTS staking: The production UI can approve optional LREP stake and submits a private up/down signal plus expected up-vote percentage through
commitVote(). Zero-LREP votes can bootstrap earned launch reputation; only staked votes carry normal settlement economics.
Key Functions
approve(votingEngine, amount)— Allow the voting engine to pull LREP stake for a prediction.lockForGovernance(account, amount)— Lock tokens for 7 days (governor only).getTransferableBalance(account)— Returns balance minus locked amount.delegate(delegatee)— Self-delegate LREP voting power; the current token rejects third-party vote delegation.
VoterIdNFT
Soulbound (non-transferable) ERC-721 representing an optional open rater identity handle. It can be wired to delegated or identity-aware flows when configured; token ID 0 is reserved (indicates no Voter ID).
Sybil Resistance
VoterIdNFT is optional for the core rating path, but gives the protocol a stable identity handle when configured for delegated voting and identity-gated flows. USDC-funded question submission is permissionless and does not require a Voter ID. Where VoterIdNFT is active, it also enforces a per-Voter-ID stake cap of 10 LREP per content per round, preventing a single identity from dominating any vote.
Key Functions
mint(holder, nullifier)— Mint a new Voter ID (authorized identity minters only).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 LREP 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 LREP 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 LREP 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 LREP, 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 launch 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. Rater weight can use optional human credentials, cluster discounts, and bonded AI declaration tiers, with the combined positive multiplier capped at 12,500 bps.
Configuration
| Parameter | Value | Description |
|---|---|---|
| MIN_STAKE | 0 LREP | Minimum vote stake; zero-LREP ratings can bootstrap earned launch reputation |
| MAX_STAKE | 10 LREP | 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
LoopReputation.approve(votingEngine, stakeAmount)thenRoundVotingEngine.commitVote(contentId, roundContext, targetRound, drandChainHash, commitHash, ciphertext, stakeAmount, frontend)— Default robust BTS flow. Locks LREP and records the tlock-encrypted up/down signal plus expected up-vote percentage. The report is hidden until the epoch ends. The redeployed contract rejects malformed or non-armored ciphertexts, binds the canonical round reference score into the round context, and binds the reveal-target metadata on-chain.commitVote(...)— Lower-level integration path for bots, tests, and direct contract callers that build the approve-plus-commit flow directly.- VoteCommitted event: emits the commit hash,
targetRound, anddrandChainHashso indexers can observe the exact reveal metadata attached to each report. The redeployed engine also snapshotsroundReferenceRatingBpsand emitsRoundConfigSnapshottedper round so every frontend can recover the exact score anchor and round settings users rated against. revealVoteByCommitKey(contentId, roundId, commitKey, isUp, predictedUpBps, salt)— Reveal a previously committed RBTS report 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, predictedUpBps, 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 leastmax(minVoters, 3)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 optional participation 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.setRaterRegistry(...)andsetRaterDeclarationRegistry(...)— Configure the optional human credential/cluster registry and the AI declaration registry used for rater-weight treatment. Setting the declaration registry to zero disables declaration weighting.
RaterDeclarationRegistry
Stores signed AI rater declarations and the operator bond that backs them. Declarations publish hashes for model family, provider, prompt template, retrieval configuration, and tooling so public users can distinguish a declared agent from an anonymous model wallet without forcing endpoint secrets on-chain. Each active or retiring declaration reserves its own operator bond capacity.
submitDeclaration(...)— Register or update a bonded declaration. Behavior-affecting changes create a new declaration version and may require a new probe.recordProbeResult(...)— Promote passing declarations toA1Verifiedor keep failed declarations atA1Unverified.openChallenge(...)andresolveChallenge(...)— Let challengers post evidence, freeze challenged declaration benefits, slash sustained false declarations, reward challengers, and demote the rater toA0. Only one open challenge can lock a declaration version, and unresolved challenges can expire after the resolver window.releaseRetiredDeclarationBond(rater)andreleaseExpiredDeclarationBond(rater)— Release a retired or expired declaration's reserved bond after the exit window elapses, so boosted commits and open challenge exposure cannot be escaped immediately.tierMultiplierBps(rater)— Read the declaration multiplier consumed by RoundVotingEngine: 10,000 bps forA0, 10,500 bps forA1Unverified, and 11,500 bps forA1Verified, falling back to 10,000 bps when the declaration is not yet effective, expired, retired, or locked by an open challenge.hasActiveAiDeclaration(rater)— Read the active AI declaration status used for human-anchor exclusion without conflating it with the reward multiplier.
Verified agent declarations are model-accountability signals, not proof-of-personhood. They can receive bounded reward-weight treatment, but they do not count as verified-human anchors for earned launch rewards or the one-time human verification bonus. Launch-anchor exclusion is based on each commit's AI declaration snapshot.
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 optional LREP participation reward for eligible winning revealed voters, using the rate snapshotted at settlement.sweepStrandedHrepToTreasury()— Governance-only recovery path for any LREP mistakenly sent directly to the distributor.
FrontendRegistry
Manages frontend operator registration and fee distribution. Frontend operators stake a fixed 1,000 LREP and receive 3% of the remaining 95% for each settled two-sided round they facilitated votes in.
Key Functions
register()— Register as frontend operator with the fixed 1,000 LREP stake.requestDeregister()/completeDeregister()— Start voluntary exit, then withdraw stake + pending fees after the unbonding window elapses.topUpStake(amount)— Restore the fixed 1,000 LREP 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. Voter ID is optional profile continuity metadata when the identity rail is configured.
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.
CuryoGovernor
OpenZeppelin Governor with timelock control. Uses LREP voting power (ERC20Votes). Tokens are locked for 7 days when proposing or casting votes.
| Parameter | Value |
|---|---|
| Voting delay | ~1 day (86,400 blocks on the 1s World Chain clock) |
| Voting period | ~1 week (604,800 blocks on the 1s World Chain clock) |
| Proposal threshold | 1,000 LREP hard floor |
| Quorum | 4% of circulating supply (min 100,000 LREP) |
| Governance lock | 7 days transfer-locked (when proposing or voting) |
| Voting delegation | self-delegated LREP only |
ParticipationPool
Implements optional participation rewards for voters when governance funds and configures a participation program. Voter rewards are claimed after round settlement using the rate snapshotted at settlement time. The launch deployment does not allocate LREP to this pool; the previous 12M LREP Bootstrap Pool allocation is folded into the LaunchDistributionPool.
Privileged sweeps of accounted participation 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 91% voters / 5% consensus subsidy / 3% 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.
- Snapshot-based governance: CuryoGovernor uses ERC20Votes snapshots for proposal voting power, and governance participation also applies a 7-day LREP transfer lock.
- Sybil Resistance: Core rating remains open, while earned launch rewards require verified-human anchored rounds and cross-round anchor diversity before payout. Per-identity stake caps, question-first submission guardrails, and claim gating apply around the reward surfaces. 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 and RoundVotingEngine 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.