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, plus the QuestionRewardPoolEscrow and FeedbackBonusEscrow custody contracts. Token, rater 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.
Protocol map
Asker or agent
Frontend and SDK
Raters
Keeper
On-chain protocol state
Indexer and public reads
| Contract | Role | Upgradeable |
|---|---|---|
| LoopReputation | ERC-20 token (LREP) with governance voting power, treasury mint controls, and governance locks | No |
| RaterRegistry | Rater identity, optional human credentials, delegation, profile follows, and verified-human anchor reads | No |
| ContentRegistry | Content lifecycle: submission, dormancy, rating updates, slashing | Transparent |
| ProtocolConfig | Governance-controlled address book and round configuration for RoundVotingEngine | Transparent |
| ClusterPayoutOracle | Governance-managed optimistic correlation epoch and round payout snapshots proposed by bonded frontend operators for USDC and launch LREP claims, with USDC challenge bonds | No |
| 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 |
| LaunchDistributionPool | 75M LREP launch distribution: 42M verified/referral, 24M anchor-gated earned rater rewards, and 9M legacy contributor vesting with 27-month claim expiry | No |
| QuestionRewardPoolEscrow | Question-scoped LREP or USDC custody, voter rewards, and the frontend-operator reward share | Transparent |
| 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 |
| RateLoopGovernor | On-chain governance with timelock (proposals, voting, execution) | No |
| RoundLib | Library: round state management and settlement logic | — |
| RewardMath | Library: Robust Bayesian Truth Serum (RBTS) score-spread settlement, forfeited-pool routing, 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 RateLoopGovernor.
- 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. - Rating stake: The production UI can approve optional LREP stake and submits a private up/down signal plus expected up-vote percentage through
commitVote(). Zero-LREP advisory votes can participate only after a round already has a staked vote; they do not count toward settlement quorum, but eligible settled advisory rounds can qualify for launch credits.
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.
RaterRegistry
The single rater identity surface. It stores optional wallet-bound human credentials, rater profile metadata, profile follows, and delegation links for cold-wallet and agent-wallet operation.
Sybil Resistance
RaterRegistry credentials are optional for the core rating path, but give the protocol a stable rater anchor for delegated voting, verified-human launch rewards, and other identity-aware flows. USDC-funded question submission is permissionless and does not require a human credential. Where identity stake caps are active, they prevent a single rater identity from dominating any vote.
Key Functions
attestHumanCredentialWithProof(root, nullifierHash, proof)— Verify a World ID v3 Proof of Human credential and attach it to the submitting wallet.attestHumanCredentialWithV4Proof(nullifier, nonce, expiresAtMin, proof)— Reserved for a future governance upgrade to World ID v4 Proof of Human.seedHumanCredential(rater, expiresAt, anchorId, evidenceHash)— Seed an approved human credential for local development, tests, or governance-admin repair.hasActiveHumanCredential(rater)/getHumanCredential(rater)— Read credential status and metadata.setProfile(raterType, metadataHash)— Publish rater metadata used by identity-aware clients.
Delegation
RaterRegistry supports delegation: a credential holder or rater identity wallet can authorize a delegate (hot wallet or agent wallet) to act on their behalf for flows that accept delegated identities, notably voting and daily profile/frontend actions. Holder-only recovery and delegation management still require the identity wallet. Setup and security guidance 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 rater identity 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 evidence and media links against CategoryRegistry before deriving the question submission key from the submitted metadata. The question-first flow accepts either a context URL or at least one public image, 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 before votes. No cancellation fee is charged. |
Key Functions
reserveSubmission(revealCommitment), thensubmitQuestionWithRewardAndRoundConfig(..., details, rewardTerms, roundConfig, spec, confidentiality)— Reserve a hidden question, then reveal it with the exact attached bounty terms, creator-selected round config, optional off-chain details URL/hash, explicit confidentiality terms, and two non-zero metadata hashes:questionMetadataHashandresultSpecHash. Question text is capped at 120 characters, the context/media/details 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 commits to its hashes incontentHashand emits optional details throughContentDetailsSubmitted. Agent asks use the same function after the user or scoped agent wallet executes the returned funding and submission calls.rewardTermsalso commits to bounty eligibility: everyone or Proof of Human for the v3 launch.rewardTerms.requiredVotersmust matchroundConfig.minVotersso a settled qualifying round is also bounty-qualifying, and bounty size can raise the required participant floor.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. Private context bundles are not accepted yet; submit gated questions individually until the uniform bundle-confidentiality path is added.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 before votes. Attached submission bounties stay non-refundable, and no cancellation fee is charged.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 rating update derived from bounded up/down signal evidence, the internal reference prior, and the conservative rating bound. Fresh content can use the internal default prior while the public UI still shows N/A until the first settlement.
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 do not receive a consensus subsidy. Commit-time reward weight is stake times the epoch timing weight; human credentials do not multiply settlement rewards.
Configuration
| Parameter | Value | Description |
|---|---|---|
| MIN_STAKE | 1 LREP | Minimum counted vote stake; zero-LREP advisory ratings route separately and do not count toward settlement quorum |
| MAX_STAKE | 10 LREP | Maximum counted vote stake per rater identity per round when identity stake caps are active |
| epochDuration | 20 minutes | Default duration of each reward tier; question creators can select within governance bounds. |
| maxDuration | 20 minutes | Default maximum round lifetime; question creators can select within governance bounds. |
| minVoters | 3 | Launch default minimum revealed votes required before settlement is allowed. Bounty voter floors can rise with bounty size: 3 below 1,000 USDC, 5 from 1,000 USDC, and 8 from 10,000 USDC. Governance can raise the default settlement voter count and the allowed minimum for new rounds as rater supply, bounty value, and attack pressure grow; already-created questions and already-open rounds keep their snapshotted configuration. |
| SCORE_SPREAD_FORFEIT_MIN_REVEALS | 8 | Minimum score-eligible revealed voters before negative score-spread LREP forfeits can apply. |
| MAX_SCORE_SPREAD_FORFEIT_BPS | 50% | Per-report cap on negative score-spread LREP forfeiture once the economic threshold is met. |
| maxVotersPerRound | 100 | 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 resolved rater identity can vote on the same content again |
Key Functions
LoopReputation.approve(votingEngine, stakeAmount)thenRoundVotingEngine.commitVote(contentId, roundContext, targetRound, drandChainHash, commitHash, ciphertext, stakeAmount, frontend)— Default private rating 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 internal rating prior into the round context, and binds the reveal-target metadata on-chain. The prior is not a user-facing vote target; raters submit an absolute thumbs-up/down signal and a separate crowd forecast.commitVote(...)— Lower-level integration path for agents, 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 rating 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, scores rating rewards from the signal and crowd forecast, and records pending public-rating evidence from bounded binary signal evidence. The visible rating moves after the finalized public-rating snapshot and veto window; bounty, launch-LREP, and public-rating correlation caps use the ClusterPayoutOracle domains for their respective paths.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, payoutWeight, proof)— Claim the USDC-backed bounty for a revealed voter after the round has a finalized correlation payout snapshot. Snapshot roots are proposed throughClusterPayoutOracleby registered frontend operators bonded with 1,000 LREP, either directly or through assigned keeper wallets, then finalized after the challenge window. Bad roots can be challenged with the configured USDC ERC20 bond, which defaults to 5 USDC (5_000_000 atomic units). New bounties default to a 3% frontend-operator share, attributed from the vote commit; unpayable frontend shares remain with the voter claim. Bounty eligibility and correlation caps gate this payout path, while a separate public-rating oracle domain controls visible rating movement from pending settlement evidence.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 that was published by the requested feedback close directly to a revealed, independent voter until the later of that close and 24 hours after settlement. The awarder pays this transaction, the recipient receives USDC or LREP immediately, and an eligible vote-attributed frontend receives the 3% share.FeedbackBonusEscrow.forfeitExpiredFeedbackBonus(poolId)— Send unawarded Feedback Bonus funds to treasury only after the effective award deadline has elapsed.cancelExpiredRound(contentId, roundId)— Cancel a round that exceeded maxDuration (20 minutes) 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. Governance can raise the default settlement voter count and the allowed minimum for new rounds as rater supply, bounty value, and attack pressure grow; already-created questions and already-open rounds keep their snapshotted configuration.
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(...),setRaterRegistry(...), andsetTreasury(...)— Maintain the engine's governance-controlled address book, including the rater identity registry used by delegation and launch-anchor policy.
RoundRewardDistributor
Pull-based reward claiming. Not pausable — users can always withdraw their tokens.
claimReward(contentId, roundId)— Claim settled-round voter payouts. Positive RBTS score spreads receive full stake plus their share of the 96% voter share of forfeited stake remaining after the settlement-caller incentive; negative spreads forfeit without a revealed-loser rebate once the score-spread economic threshold is met.sweepStrandedLrepToTreasury()— 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% for each settled two-sided round they facilitated votes in. This global operator bond also backs optimistic payout-root proposals; the oracle design relies on public artifacts, challenge windows, governance arbitration, and possible slashing or future-income loss rather than fully collateralizing each snapshot on-chain. Fee withdrawals are delayed behind a 21-day slashable review window and successful oracle challengers receive a fixed share of slash proceeds, so accountability scales with an operator's actual earnings instead of requiring per-snapshot bonds.
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.setSnapshotProposer(proposer)andclearSnapshotProposer()— Assign or clear a separate operational wallet for payout-root proposal transactions while the frontend operator remains bonded.requestFeeWithdrawal()/completeFeeWithdrawal()— Two-step withdrawal of accumulated platform fees while healthy, fully bonded, and not exiting. The requested amount stays in the registry and remains fully slashable for a 21-day review window before it can be completed, so the fee stream works as collateral that grows with the operator's usage.slashFrontend(address, amount, reason)— Slash frontend stake (governance). Already accrued frontend fees and any pending fee withdrawal are confiscated to the protocol at the same time.slashFrontendWithBounty(address, amount, reason, bountyRecipient)— Same asslashFrontend, but routes a fixed 50% of everything confiscated to the successful ClusterPayoutOracle challenger named by governance, keeping the challenge path economically live. The share is deliberately below 100% so a proposer cannot recover its own collateral by self-challenging through a fresh wallet.
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 gradient seed override. RaterRegistry provides the optional identity and credential context used alongside public profile metadata.
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 gradient seed override.getAvatarAccent(address)— Read whether an avatar gradient seed override is set and the stored RGB value.
RateLoopGovernor
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 (43,200 blocks on the 2s target-chain clock) |
| Voting period | ~1 week (302,400 blocks on the 2s target-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 |
Libraries
RewardMath
- RBTS score-spread settlement compares each revealed report's scoreBps with a leave-one-out benchmark from the other score-eligible revealed reports. Positive spreads receive full stake plus the 96% voter share of forfeited stake remaining after the settlement-caller incentive; negative spreads forfeit without a revealed-loser rebate. Score-spread LREP forfeits are disabled below 8 score-eligible revealed voters and capped at 50% of each report's stake once active.
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 redeployed rating path usesRatingMath.applySettlementwith cumulative bounded thumbs-up/down evidence, so the public score is the settled thumbs-up evidence share across all settled rounds.
RoundLib
Helpers for round state management: tracks round lifecycle (Open, Settled, Cancelled, Tied, RevealFailed) and settlement logic.