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.
| Area | Current Branch Update | Why It Matters |
|---|
| ProtocolConfig | Introduced as a transparent proxy and wired into RoundVotingEngine. | Expands the production contract surface and adds a new upgradeable configuration address book. |
| Proxy architecture | The deployment flow is transparent-proxy + ProxyAdmin based, not UUPS based. | Upgrade authorization and storage-layout claims must match the actual deployment model. |
| Reward accounting | Submitter/participation reward reservation and distribution logic changed after March 11. | These paths hold value and therefore deserve fresh targeted testing and static analysis. |
| HumanFaucet | The 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.
| Severity | Issue | Current Assessment |
|---|
| Medium | Submitter stake resolution can be griefed by keeping a round open | Fixed on current branch |
| Medium | Participation reward snapshots can become permanently unclaimable after settlement-side-effect failure | Fixed on current branch |
| Low | Category and profile registries still treat delegates as standalone Voter ID holders | Fixed on current branch |
| Medium | Dormancy could zero a healthy participation reward if stake resolution had not happened first | Fixed on current branch |
| Medium | Governance locks did not require the account to still hold the locked balance | Fixed on current branch |
| Low | VoterIdNFT and CategoryRegistry were pinned to the original governance addresses | Fixed on current branch |
| Low | Frontend registry pagination retained exited operators and could duplicate re-registrations | Fixed on current branch |
What Changed
- 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.
- 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.
- Auxiliary registries now require the holder address when Voter ID is configured. Delegates can no longer act as standalone category/profile owners.
- 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.
- 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.
- Governance migrations are now supported in the non-upgradeable registries. VoterIdNFT and CategoryRegistry can both retarget their governor/timelock references before governance ownership migrates.
- 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.
| Severity | Found | Status |
|---|
| Critical | 3 | 3 tested (invariant fuzzing) |
| High | 16 | 12 resolved, 3 verified, 1 design |
| Medium | 21 | 15 resolved/verified, 3 design, 1 needs verification, 2 accepted |
| Low | 11 | 7 resolved, 2 accepted, 2 design |
| Informational | 10 | 8 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.
| Contract | Type | Role |
|---|
| RoundVotingEngine | Transparent proxy | Core voting: tlock commit-reveal, epoch-weighted rewards, deterministic settlement, consensus subsidy |
| RoundRewardDistributor | Transparent proxy | Pull-based reward claiming |
| ContentRegistry | Transparent proxy | Content lifecycle, submitter stakes, ratings |
| ProtocolConfig | Transparent proxy | Governance-controlled protocol address book, round config, reveal grace period |
| FrontendRegistry | Transparent proxy | Frontend operator staking and fee distribution |
| ProfileRegistry | Transparent proxy | User profiles and name uniqueness |
| CategoryRegistry | Non-upgradeable | Category governance and domain uniqueness |
| CuryoReputation | Non-upgradeable | ERC-20 token with governance locking |
| VoterIdNFT | Non-upgradeable | Soulbound sybil resistance, delegation, stake limits |
| ParticipationPool | Non-upgradeable | Halving-tier participation rewards |
| HumanFaucet | Non-upgradeable | Self.xyz verified claims, referrals, Pausable |
| SubmissionCanonicalizer | Non-upgradeable | Stateless URL/domain canonicalization helper used during content submission |
| CuryoGovernor | Non-upgradeable | OpenZeppelin Governor with timelock |
| RewardMath | Library | Pool split arithmetic and reward calculations |
| RoundLib | Library | Round states, timing, settlement probability |
| CategoryFeeLib | Library | Category-fee settlement helpers |
| SubmitterStakeLib | Library | Submitter stake return/slash helpers |
| TokenTransferLib | Library | Narrow 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 verification —
forge 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
| ID | Finding | Contract | Status |
|---|
| C-01 | Pool 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). | RoundRewardDistributor | Tested |
| C-02 | Token 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. | RoundVotingEngine | Tested |
| C-03 | VotingEngine 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. | RoundVotingEngine | Tested |
High
| ID | Finding | Contract | Status |
|---|
| H-01 | cancelContent 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. | ContentRegistry | Resolved |
| H-02 | markDormant 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. | ContentRegistry | Resolved |
| H-03 | Governance 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). | CuryoReputation | Resolved |
| H-04 | Missing __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, ProfileRegistry | Resolved |
| H-05 | Zero-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, RewardMath | Resolved |
| H-06 | Critical 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. | RoundVotingEngine | Resolved |
| H-07 | MAX_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. | RoundVotingEngine | Resolved |
| H-08 | CategoryRegistry 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. | CategoryRegistry | Resolved |
| H-09 | transferReward() 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. | RoundVotingEngine | Verified |
| H-10 | ContentRegistry 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. | ContentRegistry | Verified |
| H-11 | Mock 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. | HumanFaucet | Resolved |
| H-12 | VoterIdNFT 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. | VoterIdNFT | Resolved |
| H-13 | Failed 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. | RoundVotingEngine | Resolved |
| H-14 | Fragile 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. | HumanFaucet | Resolved |
| H-15 | Proxy-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 contracts | Verified |
| H-16 | Governance 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. | CuryoReputation | Design |
Medium
| ID | Finding | Contract | Status |
|---|
| M-01 | Unbounded iteration in batch refund processing. Added startIndex and count parameters for batched processing. Keepers can call in multiple transactions for any size array. | RoundVotingEngine | Resolved |
| M-02 | ContentRegistry now has ReentrancyGuard. Added nonReentrant to submitContent, cancelContent, markDormant, and reviveContent. | ContentRegistry | Resolved |
| M-03 | Content submission spam mitigated. Cancellation now charges a 1 cREP fee and clears the URL flag so cancelled URLs can be resubmitted by legitimate users. | ContentRegistry | Resolved |
| M-04 | Settlement 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. | RoundVotingEngine | Mitigated |
| M-05 | Transparent 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 contracts | Resolved |
| M-06 | Content 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. | ContentRegistry | Verified |
| M-07 | Dormant 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. | ContentRegistry | Resolved |
| M-08 | Lock 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. | CuryoReputation | Verified |
| M-09 | Nullifier 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. | VoterIdNFT | Design |
| M-10 | VoterIdNFT 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). | VoterIdNFT | Verified |
| M-11 | Referrer 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. | HumanFaucet | Resolved |
| M-12 | No 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). | HumanFaucet | Verified |
| M-13 | ParticipationPool 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%. | ParticipationPool | Verified |
| M-14 | Retired 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. | CategoryRegistry | Superseded |
| M-15 | Governor 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. | CuryoGovernor | Verified |
| M-16 | registeredFrontends unbounded array growth. Frontend addresses are pushed to registeredFrontends but never removed, even after deregistration. Mitigated by pagination support for practical use. | FrontendRegistry | Accepted |
| M-17 | initializeV2/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. | RoundVotingEngine | Verified |
| M-18 | Cannot 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. | RoundVotingEngine | Resolved |
| M-19 | Consensus 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. | RoundVotingEngine | Design |
| M-20 | No 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. | VoterIdNFT | Accepted |
| M-21 | ERC2612 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. | RoundVotingEngine | Design |
Low
| ID | Finding | Contract | Status |
|---|
| L-01 | Cancellation fee sink must be configured. cancelContent() requires a nonzero fee-sink address so the 1 cREP anti-spam fee cannot be stranded during withdrawals. | ContentRegistry | Resolved |
| L-02 | Frontend fees for unregistered frontends stuck. Changed creditFees() from silently ignoring unregistered frontends to reverting with "Frontend not registered", preventing silent token loss. | FrontendRegistry | Resolved |
| L-03 | Domain 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. | CategoryRegistry | Resolved |
| L-04 | No token recovery function on HumanFaucet. Added withdrawRemaining(address, uint256) with onlyOwner modifier to allow recovery of remaining cREP after faucet decommissioning. | HumanFaucet | Resolved |
| L-05 | Slashed frontend tokens stuck if VotingEngine not set. Added require(address(votingEngine) != address(0)) at the start of slashFrontend() to prevent tokens from being stuck. | FrontendRegistry | Resolved |
| L-06 | Bonus 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. | RoundVotingEngine | Resolved |
| L-07 | Pool split rounding dust. Individual claim calculations using integer division can leave up to n-1 wei unclaimed. Standard and benign in Solidity parimutuel systems. | RewardMath | Accepted |
| L-08 | Dual-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. | CuryoReputation | Design |
| L-09 | Self-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. | HumanFaucet | Accepted |
| L-10 | Treasury 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. | RoundVotingEngine | Resolved |
| L-11 | Tier 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. | ParticipationPool | Design |
Informational
| ID | Finding | Contract | Status |
|---|
| I-01 | No 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. | VoterIdNFT | Resolved |
| I-02 | No fuzz tests for RewardMath. The core arithmetic library lacked fuzz tests. Property-based testing now verifies conservation invariants under random inputs. | RewardMath | Resolved |
| I-03 | Integration 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 tests | Resolved |
| I-04 | Unbounded view function arrays. Historical full-array enumeration on ProfileRegistry and CategoryRegistry has been removed, and scalable callers should use paginated enumeration. | ProfileRegistry, CategoryRegistry | Resolved |
| I-05 | Slither: abi.encodePacked collision risk. ContentRegistry.submitContent previously used encodePacked with multiple dynamic args for content hashing. Now uses abi.encode instead, eliminating collision risk. | ContentRegistry | Resolved |
| I-06 | Slither: unchecked return values. token.approve() return values were ignored in CategoryRegistry.rejectCategory and FrontendRegistry.slashFrontend. Now uses SafeERC20 forceApprove. | CategoryRegistry, FrontendRegistry | Resolved |
| I-07 | Slither: missing zero-address checks. CuryoReputation.setGovernor and setContentVotingContracts now validate against address(0). | CuryoReputation | Resolved |
| I-08 | VotingEngine should inherit interface. The interface exists but the contract did not explicitly implement it. Now inherits IRoundVotingEngine. | RoundVotingEngine | Resolved |
| I-09 | Vote 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. | RoundVotingEngine | Design |
| I-10 | settleRound 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. | RoundVotingEngine | Design |
Upgrade Safety
Storage layouts verified via forge inspect for all 6 transparent-proxy-backed contracts:
| Contract | Slots Used | Gap Size | Status |
|---|
| ContentRegistry | 0–18 (19 slots) | __gap[42] | Pass |
| RoundVotingEngine | 0–28 (29 slots) | __gap[50] | Pass |
| RoundRewardDistributor | 0–13 (14 slots) | __gap[41] | Pass |
| FrontendRegistry | 0–6 (7 slots) | __gap[49] | Pass |
| ProfileRegistry | 0–4 (5 slots) | __gap[49] | Pass |
| ProtocolConfig | 0–8 (9 slots) | __gap[50] | Pass |
| Check | Result |
|---|
| _disableInitializers() in implementation constructor | Pass — All 6 proxy-backed contracts |
| ProxyAdmin owner is governance | Pass — UpgradeTest covers authorized and unauthorized upgrades across all 6 proxy-backed contracts |
| ReentrancyGuard under proxy | Pass — 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.
| Attack | Cost | Impact | Status |
|---|
| 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 impact | Mitigated |
| 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 fee | Reduced: 1 cREP cost per spam cycle | Mitigated |
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 only | No impact: any party can attempt settlement | Mitigated |
| 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 stake | Blocked: cancel/dormancy checks vote state | Resolved |
| 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 response | Deployment |
| Settlement ordering — Keepers settle rounds in specific order to manipulate outcomes. | Gas only | No impact: round pools are 100% content-specific, no shared pool affected by ordering | No Issue |
| Participation pool drain via losing votes — Vote across many content items to earn participation rewards, regardless of round outcome. | 1–100 cREP per vote | At 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 only | Reduced: 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:
| Flow | Path | Status |
|---|
| Vote → settle → claim | User → VotingEngine → (settlement splits) → RewardDistributor → User | Verified |
| Consensus subsidy | ConsensusReserve → VotingEngine (one-sided round payouts) | Verified |
| Content submit → return/slash | User → ContentRegistry → (return to User OR slash to Treasury) | Verified |
| Frontend slash → consensus reserve | FrontendRegistry → forceApprove → RoundVotingEngine consensus reserve | Verified |
| Category rejection → consensus reserve | CategoryRegistry → forceApprove → RoundVotingEngine consensus reserve | Verified |
| Settlement callback chain | settleRound() makes external calls: safeTransfer to treasury, frontendRegistry; registry.updateRatingDirect(); registry.returnSubmitterStakeWithRewardRate()/slashSubmitterStake() | Verified — nonReentrant blocks re-entry |
Trust Assumptions
| Trusting Contract | Trusted Contract | Assumption | Protection |
|---|
| ContentRegistry | RoundVotingEngine | Only valid calls to updateRating/returnStake/slashStake | msg.sender check + CONFIG_ROLE governance |
| RoundVotingEngine | RoundRewardDistributor | Only valid calls to transferReward | Address check + CONFIG_ROLE governance |
| VoterIdNFT | RoundVotingEngine | Correct stake recording | stakeRecorder set by owner (governance) |
| VoterIdNFT | HumanFaucet | Only verified humans get VoterIDs | authorizedMinters set by owner (governance) |
| CuryoReputation | CuryoGovernor | Only governance can lock tokens | governor address set by CONFIG_ROLE |
| ParticipationPool | VotingEngine + ContentRegistry | Only authorized contracts trigger rewards | authorizedCallers mapping set by owner |
Key Invariants
- Pool split conservation: voterShare + submitterShare + platformShare == losingPool (remainder pattern, exact).
- Voter reward summation: Sum of individual claims ≤ pool (integer division floors, dust remains in contract).
- Round state transitions: Open → Settled/Cancelled/Tied/RevealFailed only. No backwards transitions.
- Double-claim prevention: rewardClaimed mapping checked before payout in RoundRewardDistributor.
- Proxy admin ownership: Governance-owned ProxyAdmins control upgrades for all 6 transparent proxies.
- Initializer protection: All proxy-backed implementations call _disableInitializers() in their constructors.
- ReentrancyGuard proxy safety: Proxy-backed runtime contracts use
ReentrancyGuardTransient, so the lock lives in transient storage rather than the persistent proxy layout. - Gas-bounded settlement: Round voters capped per content (enforced at vote time); O(1) settlement gas cost.
- Soulbound enforcement: VoterIdNFT _update override blocks all non-mint transfers; approve/setApprovalForAll revert.
- Governance lock O(1): Single aggregate GovernanceLock per address replaces unbounded array.
- Self-delegation only: CuryoReputation._delegate requires delegatee == account.
- 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.
- HumanFaucet Pausable: customVerificationHook checks _requireNotPaused(). withdrawRemaining is NOT paused (emergency fund extraction always works).
Test Coverage
1032 tests across 41 test suites.
| Test Suite | Tests | Coverage Area |
|---|
| RoundVotingEngineBranchesTest | 71 | Voting engine branch coverage |
| VoterIdNFTTest | 63 | Soulbound NFT, delegation, multi-minter, stake caps |
| FrontendRegistryCoverageTest | 49 | Frontend registry coverage |
| ContentRegistryCoverageTest | 47 | Content lifecycle, cancel, dormancy, rating |
| ParticipationPoolTest | 47 | Participation rewards, halving tiers, pool depletion |
| HumanFaucetTest | 47 | Claims, halving, referrals, Pausable |
| RoundSettlementEdgeCaseTest | 43 | Settlement edge cases, tied rounds, cancellations |
| CategoryRegistryTest | 40 | Category lifecycle, governance, pagination |
| RoundIntegrationTest | 36 | Full vote/settle/claim cycles |
| HumanFaucetCoverageTest | 35 | Faucet branch and edge case coverage |
| SettlementEdgeCasesTest | 31 | Settlement edge cases |
| RoundSettlementEdgeCase3Test | 30 | Additional settlement edge cases |
| RewardMathTest | 30 | Pool splits, voter rewards, rating delta (fuzz) |
| ContentRegistryBranchesTest | 28 | Content registry branch coverage |
| FrontendRegistryTest | 28 | Frontend staking, fees, slashing |
| HumanFaucetBranchTest | 27 | Branch coverage for faucet edge cases |
| ProfileRegistryTest | 27 | Profile names, uniqueness, pagination |
| HumanFaucetTierEdgeCaseTest | 26 | Faucet tier edge cases |
| SecurityAccessControlTest | 23 | Access control for all contracts |
| GovernanceTest | 23 | Governor, timelock, locking |
| CuryoReputationBranchesTest | 23 | Token branch coverage |
| CuryoReputationCoverageTest | 22 | Token governance locks, delegation |
| ParticipationPoolBranchesTest | 22 | Participation pool branch coverage |
| UpgradeTest | 25 | Transparent proxy auth, reinitialization, state preservation |
| HumanFaucetCoverageTest (CoverageGaps) | 21 | Additional faucet coverage |
| RoundSettlementBranchTest | 20 | Settlement branch coverage |
| FrontendRegistryBranchTest | 20 | Frontend registry branch coverage |
| FrontendRegistryEdgeCaseTest | 20 | Frontend registry edge cases |
| FrontendRegistryCoverageTest (CoverageGaps) | 16 | Additional frontend coverage |
| RoundRewardDistributorBranchesTest | 14 | Reward distributor branch coverage |
| NormalizeDomainTest | 14 | Category domain normalization |
| SelfOppositionProfitabilityTest | 10 | Self-opposition profitability across all participation tiers |
| FormalVerification_RoundLifecycle | 12 | Round state machine formal properties |
| FormalVerification_ParticipationPool | 10 | Participation pool invariants |
| FormalVerification_Governance | 10 | Governance quorum, locking properties |
| GameTheoryImprovementsTest | 5 | Game theory improvements |
| SecurityPermitTest | 5 | ERC2612 permit security |
| SecurityReentrancyTest | 4 | Reentrancy protection |
| SecuritySettlementTimingTest | 4 | Settlement timing conditions |
| GovernanceOwnableTest | 2 | Ownership restrictions |
| CategoryRegistryBranchesTest | 2 | Category registry branch coverage |
Formal Invariant Properties
Properties that must always hold. Recommended for implementation as Foundry invariant tests (stateful fuzzing):
| ID | Invariant | Severity | Status |
|---|
| INV-01 | Token conservation. For any terminal round: SUM(vote stakes) == SUM(claimed rewards) + SUM(fees) + dust | Critical | Tested |
| INV-02 | VotingEngine solvency. balanceOf(votingEngine) ≥ all pending obligations at all times | Critical | Tested |
| INV-03 | Pool split conservation. losingPool == voterShare + submitterShare + platformShare + treasuryShare (verified in RewardMath fuzz tests) | Critical | Tested |
| INV-04 | No double claims. claimReward succeeds at most once per (contentId, roundId, voter) | High | Tested |
| INV-05 | Round state finality. Once Settled/Cancelled/Tied/RevealFailed, state never changes | High | Verified |
| INV-06 | Submitter stake singularity. submitterStakeReturned transitions false→true exactly once per contentId | High | Verified |
| INV-07 | MAX_STAKE enforcement. _epochContentStake[contentId][epochId][tokenId] ≤ 100e6 at all times | Medium | Verified |
| INV-08 | MAX_SUPPLY enforcement. crepToken.totalSupply() ≤ 100,000,000e6 at all times | Medium | Verified |
Recommendations
Before Mainnet
- Configure VoterIdNFT in all contracts before enabling public access.
- Set timelock minimum delay to an appropriate value (e.g., 2 days) for governance proposals.
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.- Verify CategoryRegistry delegation (M-14) — ensure deployment script delegates tokens to the contract.
Review dormant URL locking (M-07) — Done. markDormant() now releases the URL hash.Test self-opposition profitability with participation pool at all tiers — Done. Formal profit/loss analysis in SelfOppositionProfitability.t.sol.
Short-Term
Replace assembly in _decodeReferrer (H-14) — Done. Packed referrer payloads are now left-padded and decoded via abi.decode.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.