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.

ContractRoleUpgradeable
LoopReputationERC-20 token (LREP) with governance voting power, ERC-1363 hooks, and governance locksNo
VoterIdNFTSoulbound ERC-721 representing optional verified-rater credentialsNo
RaterRegistryOptional human credentials, rater profiles, trust seeds, and cluster-score discountsNo
RaterDeclarationRegistryBonded AI model/operator declarations, probes, drift flags, challenges, and slashingNo
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
ParticipationPoolOptional governance-funded participation rewards used by voter reward claimsNo
LaunchDistributionPool64M LREP launch rewards: 35M verified/referral, 25M anchor-gated earned rater, and 4M legacy usersNo
QuestionRewardPoolEscrowQuestion-scoped LREP 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
CuryoGovernorOn-chain governance with timelock (proposals, voting, execution)No
RoundLibLibrary: round state management and settlement logic
RewardMathLibrary: 5% revealed-loser rebate, 91/3/1/5 remaining-pool split, and reward calculations
TokenTransferLibLibrary: 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.

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 LREP 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 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

ParameterValueDescription
MIN_STAKE0 LREPMinimum vote stake; zero-LREP ratings can bootstrap earned launch reputation
MAX_STAKE10 LREPMaximum 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

  • LoopReputation.approve(votingEngine, stakeAmount) then RoundVotingEngine.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, and drandChainHash so indexers can observe the exact reveal metadata attached to each report. The redeployed engine also snapshots roundReferenceRatingBps and emits RoundConfigSnapshotted per 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 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 max(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 (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.
  • setRaterRegistry(...) and setRaterDeclarationRegistry(...) — 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 to A1Verified or keep failed declarations at A1Unverified.
  • openChallenge(...) and resolveChallenge(...) — Let challengers post evidence, freeze challenged declaration benefits, slash sustained false declarations, reward challengers, and demote the rater to A0. Only one open challenge can lock a declaration version, and unresolved challenges can expire after the resolver window.
  • releaseRetiredDeclarationBond(rater) and releaseExpiredDeclarationBond(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 for A0, 10,500 bps for A1Unverified, and 11,500 bps for A1Verified, 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, 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.

CuryoGovernor

OpenZeppelin Governor with timelock control. Uses LREP voting power (ERC20Votes). Tokens are locked for 7 days when proposing or casting votes.

ParameterValue
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 threshold1,000 LREP hard floor
Quorum4% of circulating supply (min 100,000 LREP)
Governance lock7 days transfer-locked (when proposing or voting)
Voting delegationself-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.