Security Audit

Consolidated internal security audit of all Curyo smart contracts covering static analysis, manual review, storage layout verification, and economic attack analysis. Consolidated from 5 prior review rounds (V1–V5, Feb 2025–Feb 2026), with a historical full follow-up contract review and full-suite test rerun on March 11, 2026, plus a current-branch refresh on March 20, 2026.

Executive Summary

The March 4 consolidated audit below captures the historical finding inventory across the earlier review rounds, and the March 11, 2026 follow-up remains the latest full manual review captured in this document. Since then the production contract surface has continued to change, so the current branch should be read as a historical audit baseline plus the current-branch addendum and validation updates below, not as a frozen March 11 snapshot.

The refresh on March 20, 2026 updates this page for the current deployment surface, transparent proxy architecture, post-March-11 contract changes, and the final H-14 hardening change in HumanFaucet._decodeReferrer().

Current Branch Addendum (March 20, 2026)

The contract surface changed materially after the March 11 follow-up review. The current branch now includes a separate ProtocolConfig proxy, proxy-admin-governed configuration wiring, storage and gas optimizations across the round system, and post-settlement reward reservation fixes that touched reward accounting and upgrade layouts.

AreaCurrent Branch UpdateWhy It Matters
ProtocolConfigIntroduced as a transparent proxy and wired into RoundVotingEngine.Expands the production contract surface and adds a new upgradeable configuration address book.
Proxy architectureThe deployment flow is transparent-proxy + ProxyAdmin based, not UUPS based.Upgrade authorization and storage-layout claims must match the actual deployment model.
Reward accountingSubmitter/participation reward reservation and distribution logic changed after March 11.These paths hold value and therefore deserve fresh targeted testing and static analysis.
HumanFaucetThe H-14 referrer-decoding hardening is now implemented on the current branch.The historical “fragile” assembly path is no longer present in production code.

Historical Follow-Up Review (March 11, 2026)

The March 11 review re-ran the full Foundry suite, performed a fresh manual audit of the then-current production contracts, and drove a remediation pass for the remaining medium/low findings on that branch. A short follow-on hardening pass then landed governance-migration hooks, live-balance enforcement for governance locks, and registry pagination cleanup.

SeverityIssueCurrent Assessment
MediumSubmitter stake resolution can be griefed by keeping a round openFixed on current branch
MediumParticipation reward snapshots can become permanently unclaimable after settlement-side-effect failureFixed on current branch
LowCategory and profile registries still treat delegates as standalone Voter ID holdersFixed on current branch
MediumDormancy could zero a healthy participation reward if stake resolution had not happened firstFixed on current branch
MediumGovernance locks did not require the account to still hold the locked balanceFixed on current branch
LowVoterIdNFT and CategoryRegistry were pinned to the original governance addressesFixed on current branch
LowFrontend registry pagination retained exited operators and could duplicate re-registrationsFixed on current branch

What Changed

  1. Submitter stake resolution no longer blocks on later open rounds. The return/slash decision now resolves once the content has a qualifying settled round, even if another round is still open afterward.
  2. Participation snapshot recovery is now repairable. Governance backfill can repair settlement-side-effect failures where the pool snapshot was written but the rate snapshot remained zero.
  3. Auxiliary registries now require the holder address when Voter ID is configured. Delegates can no longer act as standalone category/profile owners.
  4. Submitter participation rewards now survive dormancy edge cases. Content dormancy no longer wipes an otherwise valid participation reward just because stake resolution had not already been materialized.
  5. Governance locks now bind live balances. Accounts must still hold the locked cREP when the governor applies a new lock, which closes the snapshot-then-transfer gap.
  6. Governance migrations are now supported in the non-upgradeable registries. VoterIdNFT and CategoryRegistry can both retarget their governor/timelock references before governance ownership migrates.
  7. Frontend pagination now tracks the active set correctly. Exited operators are removed from pagination and re-registering the same operator no longer creates duplicates.

The table below remains the historical March 4, 2026 consolidated finding inventory from review rounds V1–V5, with statuses updated where the current branch has since addressed the original issue.

SeverityFoundStatus
Critical33 tested (invariant fuzzing)
High1612 resolved, 3 verified, 1 design
Medium2115 resolved/verified, 3 design, 1 needs verification, 2 accepted
Low117 resolved, 2 accepted, 2 design
Informational108 resolved, 2 design

Scope

The current production contract surface includes 13 deployed contracts and 5 supporting libraries. Six contracts are deployed behind transparent proxies with governance-owned ProxyAdmins; mock and test contracts are excluded.

ContractTypeRole
RoundVotingEngineTransparent proxyCore voting: tlock commit-reveal, epoch-weighted rewards, deterministic settlement, consensus subsidy
RoundRewardDistributorTransparent proxyPull-based reward claiming
ContentRegistryTransparent proxyContent lifecycle, submitter stakes, ratings
ProtocolConfigTransparent proxyGovernance-controlled protocol address book, round config, reveal grace period
FrontendRegistryTransparent proxyFrontend operator staking and fee distribution
ProfileRegistryTransparent proxyUser profiles and name uniqueness
CategoryRegistryNon-upgradeableCategory governance and domain uniqueness
CuryoReputationNon-upgradeableERC-20 token with governance locking
VoterIdNFTNon-upgradeableSoulbound sybil resistance, delegation, stake limits
ParticipationPoolNon-upgradeableHalving-tier participation rewards
HumanFaucetNon-upgradeableSelf.xyz verified claims, referrals, Pausable
SubmissionCanonicalizerNon-upgradeableStateless URL/domain canonicalization helper used during content submission
CuryoGovernorNon-upgradeableOpenZeppelin Governor with timelock
RewardMathLibraryPool split arithmetic and reward calculations
RoundLibLibraryRound states, timing, settlement probability
CategoryFeeLibLibraryCategory-fee settlement helpers
SubmitterStakeLibLibrarySubmitter stake return/slash helpers
TokenTransferLibLibraryNarrow token transfer helpers used by reward settlement flows

Historical Methodology

  • Static analysis — Slither on all contracts with dependency filtering. Summary: 1 high, 8 medium, 30 low, 63 informational (most from OZ dependencies or informational patterns).
  • Manual review — Line-by-line review of the production contract and library surface: token flows, state transitions, access control, and upgrade safety.
  • Storage layout verificationforge inspect on all proxy-backed contracts to verify append-only layouts, reserved gaps, and no collisions.
  • Economic analysis — Game-theoretic attack scenarios against the parimutuel voting mechanism.
  • Dependency audit — OpenZeppelin v5.5.0 compatibility verification for upgradeable proxies.

Iterative review across 5 rounds (V1–V5) plus final consolidation. Updated for the tlock commit-reveal + epoch-weighted settlement architecture. New round-based findings from inline audit notes incorporated.


Historical Findings

Critical

IDFindingContractStatus
C-01Pool solvency — reward claims must not exceed VotingEngine balance. Reward is (stake / totalWinStake) × pool. Due to integer division rounding down, the sum of all claims ≤ pool. The VotingEngine holds both winning stakes (returned to winners) and losing pool tokens (distributed as rewards). Algebraically correct. Verified via stateful invariant fuzzing (invariant_C01_PoolSolvency in InvariantSolvency.t.sol).RoundRewardDistributorTested
C-02Token conservation invariant. For any round that reaches a terminal state: SUM(vote stakes) must equal SUM(claimed rewards) + SUM(platform fees) + SUM(treasury fees) + SUM(submitter rewards) + dust. Verified via stateful invariant fuzzing (invariant_C02_TokenConservation in InvariantSolvency.t.sol). Ghost variables track all token flows.RoundVotingEngineTested
C-03VotingEngine balance solvency invariant. At any point: crepToken.balanceOf(votingEngine) must be ≥ SUM(open round stakes) + SUM(unclaimed winner rewards) + SUM(unclaimed refunds) + SUM(unclaimed submitter rewards). Verified via stateful invariant fuzzing (invariant_C03_BalanceSolvency in InvariantSolvency.t.sol). Checks engine balance against computed obligations after random vote/settle/claim sequences.RoundVotingEngineTested

High

IDFindingContractStatus
H-01cancelContent with active votes strands voter stakes. Submitter can cancel content after voters have voted, preventing settlement (isActive check fails). Voter stakes forfeit. Fix: cancelContent now reverts if any votes have been cast.ContentRegistryResolved
H-02markDormant lacks vote check. Anyone can mark active content dormant after 30 days of inactivity, even with an active open round, blocking settlement. Fix: markDormant reverts if an active open round exists.ContentRegistryResolved
H-03Governance lock array unbounded. Every governance vote appended to an array iterated on every token transfer. After many votes, transfers can exceed gas limits. Replaced with a single aggregate lock per address (O(1) reads/writes).CuryoReputationResolved
H-04Missing __gap on 3 proxy-backed contracts. ContentRegistry, FrontendRegistry, and ProfileRegistry lacked storage gap variables, risking storage collisions on future upgrades. All three contracts now include uint256[50] private __gap.ContentRegistry, FrontendRegistry, ProfileRegistryResolved
H-05Zero-cost rating manipulation. Unopposed down votes update content rating at no cost (stake returned). Rating now uses a smoothed stake-imbalance formula with a fixed 50 cREP parameter, so low-stake unanimous rounds only move rating slightly and large swings require materially larger revealed stake imbalance.RoundVotingEngine, RewardMathResolved
H-06Critical functions missing whenNotPaused. Settlement was previously callable during an emergency pause. The current settleRound path is protected by whenNotPaused, so paused state now blocks settlement side effects as intended.RoundVotingEngineResolved
H-07MAX_VOTERS cap enforced at vote time. Cap is enforced when the vote is cast, preventing users from losing stakes through no fault of their own.RoundVotingEngineResolved
H-08CategoryRegistry missing ReentrancyGuard. FrontendRegistry has ReentrancyGuard with nonReentrant on all state-changing functions. CategoryRegistry did not, despite having similar token transfer patterns. Added ReentrancyGuard and nonReentrant to submitCategory, approveCategory, rejectCategory.CategoryRegistryResolved
H-09transferReward() uses address check, not role modifier. require(msg.sender == rewardDistributor) instead of onlyRole. If CONFIG_ROLE holder calls setRewardDistributor() to a malicious address, all VotingEngine cREP can be drained. In production, CONFIG_ROLE is held by governance timelock with 2-day delay, providing community response window.RoundVotingEngineVerified
H-10ContentRegistry callback trust boundary. Functions updateRating, returnSubmitterStake, slashSubmitterStake use require(msg.sender == votingEngine). The votingEngine address is set via setVotingEngine() which requires CONFIG_ROLE and non-zero address. Same governance protection as H-09.ContentRegistryVerified
H-11Mock mode disabled on non-local chains. The mock verification mode for HumanFaucet must be restricted to local development chains only. Production deployment must enforce real Self.xyz verification.HumanFaucetResolved
H-12VoterIdNFT identity chaining prevented. A user with an existing VoterID cannot mint a second one. The customVerificationHook checks addressClaimed[msg.sender] before minting, preventing identity chaining via delegation.VoterIdNFTResolved
H-13Failed refund handling in batch processing. If a token transfer fails during batch processing of cancelled round refunds, the entire batch reverts. Individual try-catch or skip logic ensures one failed refund does not block processing of remaining claims.RoundVotingEngineResolved
H-14Fragile referrer decoding in _decodeReferrer(). The legacy implementation relied on inline assembly to read packed address payloads. The current branch now left-pads the first 20 bytes into a 32-byte ABI word and decodes via abi.decode, preserving packed-input compatibility without the assembly path.HumanFaucetResolved
H-15Proxy-safe guard usage in proxy-backed runtime contracts. The current runtime contracts use OpenZeppelin's ReentrancyGuardTransient, which stores the lock in transient storage rather than a normal persistent storage slot. That avoids layout collisions with transparent proxy state while still blocking same-transaction reentrancy.Proxy-backed runtime contractsVerified
H-16Governance lock exemption for content voting. CuryoReputation._update() allows transfers TO votingEngine and contentRegistry even when tokens are governance-locked. The same tokens can be "locked for governance" and "staked in content voting" simultaneously. Intentional design: governance participation should not block content voting.CuryoReputationDesign

Medium

IDFindingContractStatus
M-01Unbounded iteration in batch refund processing. Added startIndex and count parameters for batched processing. Keepers can call in multiple transactions for any size array.RoundVotingEngineResolved
M-02ContentRegistry now has ReentrancyGuard. Added nonReentrant to submitContent, cancelContent, markDormant, and reviveContent.ContentRegistryResolved
M-03Content submission spam mitigated. Cancellation now charges a 1 cREP fee and clears the URL flag so cancelled URLs can be resubmitted by legitimate users.ContentRegistryResolved
M-04Settlement timing manipulation. Mitigated by design: settleRound is permissionless once minVoters is reached and past-epoch reveal constraints are satisfied. A single keeper cannot settle early or bypass the reveal gate.RoundVotingEngineMitigated
M-05Transparent proxy upgrade tests added. All 6 proxy-backed contracts now have upgrade path tests covering governance-owned ProxyAdmin authorization, reinitialization prevention, state preservation after upgrade, and implementation direct-initialization protection.All proxy-backed contractsResolved
M-06Content status transitions verified. Valid: Active→Dormant/Cancelled, Dormant→Active(revive). Invalid transitions blocked by status checks. Double return/slash prevented by submitterStakeReturned flag checked in all relevant paths.ContentRegistryVerified
M-07Dormant URL stays locked. The canonical submission key was cleared on cancel but not on dormancy. Dormant URLs staying locked prevents legitimate resubmission. Fix:markDormant() now releases the submission key so the content can be resubmitted.ContentRegistryResolved
M-08Lock accumulation without cap. lockForGovernance() accumulates: lock.amount += amount. Multiple governance votes/proposals stack. If locked amount exceeds balance, getTransferableBalance() returns 0 (no underflow). Locks expire 7 days from last update.CuryoReputationVerified
M-09Nullifier stays used after VoterID revocation. When a VoterID is revoked, the nullifier remains marked as used. Prevents "revoke-and-re-register" abuse but also blocks legitimate users who are wrongly revoked. Governance can mint a new VoterID directly if needed.VoterIdNFTDesign
M-10VoterIdNFT recordStake() has no cap. recordStake() accumulates _epochContentStake without checking MAX_STAKE. The cap is enforced by RoundVotingEngine at vote time. If stakeRecorder is changed to a buggy contract, the cap could be bypassed. stakeRecorder is set by owner (governance).VoterIdNFTVerified
M-11Referrer validation doesn't check revoked VoterID. The actual claim logic at customVerificationHook only checks addressClaimed[referrer]. A user whose VoterID has been revoked can still serve as a referrer if they previously claimed. Fix: referrer validation now checks hasVoterId(referrer); revoked referrers produce no bonus. Tested in test_Referral_RevokedVoterIdReferrer_NoBonus.HumanFaucetResolved
M-12No ReentrancyGuard on HumanFaucet. Safe because: (1) entry is via Self.xyz hub callback, (2) ERC20 transfer has no recipient callback, (3) customVerificationHook is internal override (cannot be called externally).HumanFaucetVerified
M-13ParticipationPool reentrancy protection. No ReentrancyGuard, but rewardVote() and rewardSubmission() are called from VotingEngine and ContentRegistry which both have nonReentrant. Halving loop: max ~14 iterations before rate floors at 1%.ParticipationPoolVerified
M-14Retired by design change. Category submissions no longer call governor.propose() from CategoryRegistry. A real wallet now sponsors the approval proposal separately and links it afterward, so the registry no longer needs standing delegated voting power.CategoryRegistrySuperseded
M-15Governor lock accumulation with multiple proposals. Creating 5 proposals locks 500 cREP for 7 days from the last proposal (timer resets). Users should be aware that governance participation has a liquidity cost that compounds.CuryoGovernorVerified
M-16registeredFrontends unbounded array growth. Frontend addresses are pushed to registeredFrontends but never removed, even after deregistration. Mitigated by pagination support for practical use.FrontendRegistryAccepted
M-17initializeV2/V3 visibility. Both are public with reinitializer(n). While publicly callable, the reinitializer modifier ensures each can only execute once. Standard transparent-proxy upgrade flow (upgrade + initialize in one transaction via ProxyAdmin) prevents front-running.RoundVotingEngineVerified
M-18Cannot cancel round if threshold reached. Once the minimum voter threshold for a round has been met, the round cannot be cancelled. Submitter cannot use cancellation to avoid negative ratings or stake loss.RoundVotingEngineResolved
M-19Consensus reserve may deplete to 0. The consensus subsidy (5% of totalStake for unanimous rounds) is drawn from a reserve. If the reserve is exhausted, unanimous rounds receive no subsidy. This is a graceful degradation — voting still works, just without the bonus.RoundVotingEngineDesign
M-20No on-chain maxSupply on VoterIdNFT. The VoterIdNFT has no on-chain cap on total supply. Supply is limited in practice by Self.xyz passport verification (one per person). If governance adds a permissive minter, unlimited VoterIDs could be minted.VoterIdNFTAccepted
M-21ERC2612 permit front-running. A front-runner can extract the permit signature from a voteWithPermit transaction and call permit() directly, consuming the nonce. The user's transaction reverts but they can retry with standard approve(). UX issue, not a fund risk. Standard ERC2612 limitation.RoundVotingEngineDesign

Low

IDFindingContractStatus
L-01Cancellation fee sink must be configured. cancelContent() requires a nonzero fee-sink address so the 1 cREP anti-spam fee cannot be stranded during withdrawals.ContentRegistryResolved
L-02Frontend fees for unregistered frontends stuck. Changed creditFees() from silently ignoring unregistered frontends to reverting with "Frontend not registered", preventing silent token loss.FrontendRegistryResolved
L-03Domain normalization incomplete in CategoryRegistry. Rewrote _normalizeDomain() to strip protocols, paths, query strings, fragments, and trailing DNS dots. All URL variants now normalize to bare domain.CategoryRegistryResolved
L-04No token recovery function on HumanFaucet. Added withdrawRemaining(address, uint256) with onlyOwner modifier to allow recovery of remaining cREP after faucet decommissioning.HumanFaucetResolved
L-05Slashed frontend tokens stuck if VotingEngine not set. Added require(address(votingEngine) != address(0)) at the start of slashFrontend() to prevent tokens from being stuck.FrontendRegistryResolved
L-06Bonus timestamp uses settlement time. Bonus calculation uses block.timestamp at settlement, not the round's active time. Wrong bonus rate could apply near the 20-year boundary.RoundVotingEngineResolved
L-07Pool split rounding dust. Individual claim calculations using integer division can leave up to n-1 wei unclaimed. Standard and benign in Solidity parimutuel systems.RewardMathAccepted
L-08Dual-purpose tokens — governance + content voting. The same cREP tokens serve both governance voting power and content voting stakes. Governance locks allow staking into the VotingEngine, meaning governance influence and content stakes are not fully independent.CuryoReputationDesign
L-09Self-referral possible with two passports. A user with two passport-verified identities can refer themselves for a 50% bonus. Limited by the cost and difficulty of obtaining multiple passports.HumanFaucetAccepted
L-10Treasury transfer try-catch for robustness. Treasury transfers during settlement use a direct safeTransfer. If the treasury address is a contract that reverts, settlement fails. Consider wrapping in try-catch for robustness.RoundVotingEngineResolved
L-11Tier transitions are discrete cliffs. ParticipationPool tier boundaries create cliff-like transitions where the last claim at a higher tier gets significantly more than the first claim at a lower tier. This is inherent to the halving design.ParticipationPoolDesign

Informational

IDFindingContractStatus
I-01No dedicated VoterIdNFT test suite. Soulbound enforcement, stake cap compliance, and nullifier deduplication were tested only indirectly. Dedicated VoterIdNFT.t.sol with 63 tests now added.VoterIdNFTResolved
I-02No fuzz tests for RewardMath. The core arithmetic library lacked fuzz tests. Property-based testing now verifies conservation invariants under random inputs.RewardMathResolved
I-03Integration tests don't configure VoterIdNFT. The full sybil-resistance flow (HumanFaucet claim → VoterIdNFT mint → vote with stake cap) was untested end-to-end. Now covered by RoundIntegration.t.sol.Integration testsResolved
I-04Unbounded view function arrays. Historical full-array enumeration on ProfileRegistry and CategoryRegistry has been removed, and scalable callers should use paginated enumeration.ProfileRegistry, CategoryRegistryResolved
I-05Slither: abi.encodePacked collision risk. ContentRegistry.submitContent previously used encodePacked with multiple dynamic args for content hashing. Now uses abi.encode instead, eliminating collision risk.ContentRegistryResolved
I-06Slither: unchecked return values. token.approve() return values were ignored in CategoryRegistry.rejectCategory and FrontendRegistry.slashFrontend. Now uses SafeERC20 forceApprove.CategoryRegistry, FrontendRegistryResolved
I-07Slither: missing zero-address checks. CuryoReputation.setGovernor and setContentVotingContracts now validate against address(0).CuryoReputationResolved
I-08VotingEngine should inherit interface. The interface exists but the contract did not explicitly implement it. Now inherits IRoundVotingEngine.RoundVotingEngineResolved
I-09Vote direction encrypted at commit time. Vote direction (up/down) is encrypted via tlock at commit time and only revealed after the epoch ends. By design: the commit-reveal model uses tlock encryption and epoch-weighted rewards to incentivize independent assessment. Commit hashes enable double-vote prevention, self-vote prevention, cooldown periods, and sybil stake limits.RoundVotingEngineDesign
I-10settleRound is permissionless. Anyone can call settleRound(contentId, roundId) once the revealed-vote threshold is reached and reveal constraints are satisfied. Any keeper or user can trigger settlement; no privileged operator controls it.RoundVotingEngineDesign

Upgrade Safety

Storage layouts verified via forge inspect for all 6 transparent-proxy-backed contracts:

ContractSlots UsedGap SizeStatus
ContentRegistry0–18 (19 slots)__gap[42]Pass
RoundVotingEngine0–28 (29 slots)__gap[50]Pass
RoundRewardDistributor0–13 (14 slots)__gap[41]Pass
FrontendRegistry0–6 (7 slots)__gap[49]Pass
ProfileRegistry0–4 (5 slots)__gap[49]Pass
ProtocolConfig0–8 (9 slots)__gap[50]Pass
CheckResult
_disableInitializers() in implementation constructorPass — All 6 proxy-backed contracts
ProxyAdmin owner is governancePass — UpgradeTest covers authorized and unauthorized upgrades across all 6 proxy-backed contracts
ReentrancyGuard under proxyPass — Proxy-backed runtime contracts use ReentrancyGuardTransient, so the lock lives in transient storage rather than the normal persistent layout.

Economic Analysis

Game-theoretic attack scenarios against the round-based parimutuel voting mechanism. All scenarios assume VoterIdNFT is active (sybil resistance enabled, 100 cREP max stake per voter per content per round). No global voter pool (100% content-specific). Consensus reserve subsidizes unanimous rounds.

AttackCostImpactStatus
Rating manipulation — Unopposed down votes move content rating. Rating delta is smoothed by a fixed 50 cREP parameter, so low-stake attacks only nudge rating and larger swings require significantly more revealed stake imbalance.1–100 cREP (returned if unopposed)Reduced: low-stake rounds have limited impactMitigated
Content spam — Submit-cancel loop to pollute the content registry. Cancellation charges a 1 cREP fee and clears the URL flag so cancelled URLs can be resubmitted.10 cREP stake + 1 cREP cancel feeReduced: 1 cREP cost per spam cycleMitigated
Settlement timing manipulation — Malicious keeper attempts to control settlement timing. Mitigated: settleRound is permissionless, but only callable once the round reaches the revealed-vote threshold and past-epoch reveal gate.Gas onlyNo impact: any party can attempt settlementMitigated
Vote stranding — Submitter cancels or content goes dormant while voters have active stakes. cancelContent reverts if votes have been cast; markDormant reverts if an active open round exists.10 cREP submitter stakeBlocked: cancel/dormancy checks vote stateResolved
VoterIdNFT bypass — If VoterIdNFT is not configured (address(0)), all sybil resistance checks are skipped.All checks skip if voterIdNFT == address(0)Deployment
Governance attack — Attacker acquires 4% of circulating supply to reach quorum and pass malicious proposals. TimelockController delay provides community response window.4% of circulating cREP (dynamic quorum, 10K floor)Timelock delay allows community responseDeployment
Settlement ordering — Keepers settle rounds in specific order to manipulate outcomes.Gas onlyNo impact: round pools are 100% content-specific, no shared pool affected by orderingNo Issue
Participation pool drain via losing votes — Vote across many content items to earn participation rewards, regardless of round outcome.1–100 cREP per voteAt tier 0: vote 100 cREP, earn 90 cREP participation reward, but losing side forfeits stake. Net loss if on losing side. At all tiers, participation reward is less than stake.Not Profitable
Self-opposition + participation pool — Vote both sides to control outcome and earn participation rewards on both votes.2 stakes (1 + 100 cREP)Tested in SelfOppositionProfitability.t.sol (10 tests). Optimal strategy (100 cREP winning / 1 cREP losing) is profitable at all participation tiers due to parimutuel voter-pool share. Equal-stakes strategy unprofitable at tier 2+. Mitigated by VoterIdNFT sybil resistance (1 identity per voter) and halving participation tiers.Tested
MAX_VOTERS cap griefing — Fill voter slots with minimum-stake sybil votes.1000 cREP (1000 VoterIDs)With VoterIdNFT: impractical (requires verified identities). Without: trivially sybilable. Cap enforced at vote time.Deployment
Coordinated rating floor attack — 41+ voters push rating below 25 to trigger submitter stake slash. 24-hour cooldown per voter per content limits repeats.41 voters × 1–100 cREP (returned if unopposed)Requires 41 verified identities over 41 rounds. Slash sends 10 cREP to treasury — no attacker profit.Accepted
MEV in settlement timing — Attempting to time settlement for favorable outcomes.Gas onlyReduced: settlement is rule-based and permissionless, so no privileged keeper controls the timing once the round is eligible.No Issue
Referral loop exploitation — Two colluding users claim with each other as referrers, extracting ~100% more cREP per pair.0 (uses faucet claims)Accelerates 78M faucet depletion by up to 2x. Expected cost of referral incentives. Referrer must have VoterIdNFT.Design

Cross-Contract Interaction Analysis

Mapped fund flow paths and trust assumptions across the protocol:

FlowPathStatus
Vote → settle → claimUser → VotingEngine → (settlement splits) → RewardDistributor → UserVerified
Consensus subsidyConsensusReserve → VotingEngine (one-sided round payouts)Verified
Content submit → return/slashUser → ContentRegistry → (return to User OR slash to Treasury)Verified
Frontend slash → consensus reserveFrontendRegistry → forceApprove → RoundVotingEngine consensus reserveVerified
Category rejection → consensus reserveCategoryRegistry → forceApprove → RoundVotingEngine consensus reserveVerified
Settlement callback chainsettleRound() makes external calls: safeTransfer to treasury, frontendRegistry; registry.updateRatingDirect(); registry.returnSubmitterStakeWithRewardRate()/slashSubmitterStake()Verified — nonReentrant blocks re-entry

Trust Assumptions

Trusting ContractTrusted ContractAssumptionProtection
ContentRegistryRoundVotingEngineOnly valid calls to updateRating/returnStake/slashStakemsg.sender check + CONFIG_ROLE governance
RoundVotingEngineRoundRewardDistributorOnly valid calls to transferRewardAddress check + CONFIG_ROLE governance
VoterIdNFTRoundVotingEngineCorrect stake recordingstakeRecorder set by owner (governance)
VoterIdNFTHumanFaucetOnly verified humans get VoterIDsauthorizedMinters set by owner (governance)
CuryoReputationCuryoGovernorOnly governance can lock tokensgovernor address set by CONFIG_ROLE
ParticipationPoolVotingEngine + ContentRegistryOnly authorized contracts trigger rewardsauthorizedCallers mapping set by owner

Key Invariants

  1. Pool split conservation: voterShare + submitterShare + platformShare == losingPool (remainder pattern, exact).
  2. Voter reward summation: Sum of individual claims ≤ pool (integer division floors, dust remains in contract).
  3. Round state transitions: Open → Settled/Cancelled/Tied/RevealFailed only. No backwards transitions.
  4. Double-claim prevention: rewardClaimed mapping checked before payout in RoundRewardDistributor.
  5. Proxy admin ownership: Governance-owned ProxyAdmins control upgrades for all 6 transparent proxies.
  6. Initializer protection: All proxy-backed implementations call _disableInitializers() in their constructors.
  7. ReentrancyGuard proxy safety: Proxy-backed runtime contracts use ReentrancyGuardTransient, so the lock lives in transient storage rather than the persistent proxy layout.
  8. Gas-bounded settlement: Round voters capped per content (enforced at vote time); O(1) settlement gas cost.
  9. Soulbound enforcement: VoterIdNFT _update override blocks all non-mint transfers; approve/setApprovalForAll revert.
  10. Governance lock O(1): Single aggregate GovernanceLock per address replaces unbounded array.
  11. Self-delegation only: CuryoReputation._delegate requires delegatee == account.
  12. Governance-first access control: Timelock holds DEFAULT_ADMIN_ROLE from deployment. Deployer has only temporary CONFIG/MINTER roles with no grant power. Ownable contracts restrict transferOwnership to immutable governance address.
  13. HumanFaucet Pausable: customVerificationHook checks _requireNotPaused(). withdrawRemaining is NOT paused (emergency fund extraction always works).

Test Coverage

1032 tests across 41 test suites.

Test SuiteTestsCoverage Area
RoundVotingEngineBranchesTest71Voting engine branch coverage
VoterIdNFTTest63Soulbound NFT, delegation, multi-minter, stake caps
FrontendRegistryCoverageTest49Frontend registry coverage
ContentRegistryCoverageTest47Content lifecycle, cancel, dormancy, rating
ParticipationPoolTest47Participation rewards, halving tiers, pool depletion
HumanFaucetTest47Claims, halving, referrals, Pausable
RoundSettlementEdgeCaseTest43Settlement edge cases, tied rounds, cancellations
CategoryRegistryTest40Category lifecycle, governance, pagination
RoundIntegrationTest36Full vote/settle/claim cycles
HumanFaucetCoverageTest35Faucet branch and edge case coverage
SettlementEdgeCasesTest31Settlement edge cases
RoundSettlementEdgeCase3Test30Additional settlement edge cases
RewardMathTest30Pool splits, voter rewards, rating delta (fuzz)
ContentRegistryBranchesTest28Content registry branch coverage
FrontendRegistryTest28Frontend staking, fees, slashing
HumanFaucetBranchTest27Branch coverage for faucet edge cases
ProfileRegistryTest27Profile names, uniqueness, pagination
HumanFaucetTierEdgeCaseTest26Faucet tier edge cases
SecurityAccessControlTest23Access control for all contracts
GovernanceTest23Governor, timelock, locking
CuryoReputationBranchesTest23Token branch coverage
CuryoReputationCoverageTest22Token governance locks, delegation
ParticipationPoolBranchesTest22Participation pool branch coverage
UpgradeTest25Transparent proxy auth, reinitialization, state preservation
HumanFaucetCoverageTest (CoverageGaps)21Additional faucet coverage
RoundSettlementBranchTest20Settlement branch coverage
FrontendRegistryBranchTest20Frontend registry branch coverage
FrontendRegistryEdgeCaseTest20Frontend registry edge cases
FrontendRegistryCoverageTest (CoverageGaps)16Additional frontend coverage
RoundRewardDistributorBranchesTest14Reward distributor branch coverage
NormalizeDomainTest14Category domain normalization
SelfOppositionProfitabilityTest10Self-opposition profitability across all participation tiers
FormalVerification_RoundLifecycle12Round state machine formal properties
FormalVerification_ParticipationPool10Participation pool invariants
FormalVerification_Governance10Governance quorum, locking properties
GameTheoryImprovementsTest5Game theory improvements
SecurityPermitTest5ERC2612 permit security
SecurityReentrancyTest4Reentrancy protection
SecuritySettlementTimingTest4Settlement timing conditions
GovernanceOwnableTest2Ownership restrictions
CategoryRegistryBranchesTest2Category registry branch coverage

Formal Invariant Properties

Properties that must always hold. Recommended for implementation as Foundry invariant tests (stateful fuzzing):

IDInvariantSeverityStatus
INV-01Token conservation. For any terminal round: SUM(vote stakes) == SUM(claimed rewards) + SUM(fees) + dustCriticalTested
INV-02VotingEngine solvency. balanceOf(votingEngine) ≥ all pending obligations at all timesCriticalTested
INV-03Pool split conservation. losingPool == voterShare + submitterShare + platformShare + treasuryShare (verified in RewardMath fuzz tests)CriticalTested
INV-04No double claims. claimReward succeeds at most once per (contentId, roundId, voter)HighTested
INV-05Round state finality. Once Settled/Cancelled/Tied/RevealFailed, state never changesHighVerified
INV-06Submitter stake singularity. submitterStakeReturned transitions false→true exactly once per contentIdHighVerified
INV-07MAX_STAKE enforcement. _epochContentStake[contentId][epochId][tokenId] ≤ 100e6 at all timesMediumVerified
INV-08MAX_SUPPLY enforcement. crepToken.totalSupply() ≤ 100,000,000e6 at all timesMediumVerified

Recommendations

Before Mainnet

  1. Configure VoterIdNFT in all contracts before enabling public access.
  2. Set timelock minimum delay to an appropriate value (e.g., 2 days) for governance proposals.
  3. Implement invariant tests for C-01, C-02, C-03 — Done. Stateful fuzz tests in InvariantSolvency.t.sol with ghost-variable accounting via VotingHandler.sol.
  4. Verify CategoryRegistry delegation (M-14) — ensure deployment script delegates tokens to the contract.
  5. Review dormant URL locking (M-07) — Done. markDormant() now releases the URL hash.
  6. Test self-opposition profitability with participation pool at all tiers — Done. Formal profit/loss analysis in SelfOppositionProfitability.t.sol.

Short-Term

  1. Replace assembly in _decodeReferrer (H-14) — Done. Packed referrer payloads are now left-padded and decoded via abi.decode.
  2. Review referrer validation (M-11) — Done. Revoked VoterID holders no longer earn referral bonuses.

This is an internal AI-assisted security review, not a professional third-party audit. Historical consolidated audit: March 4, 2026. Historical follow-up review: March 11, 2026. Current-branch refresh and final H-14 hardening: March 20, 2026.