Skip to content

fix: Correct voting algorithms and restore anonymous voting security#241

Open
Tech-Psycho95 wants to merge 1 commit intoAOSSIE-Org:mainfrom
Tech-Psycho95:main
Open

fix: Correct voting algorithms and restore anonymous voting security#241
Tech-Psycho95 wants to merge 1 commit intoAOSSIE-Org:mainfrom
Tech-Psycho95:main

Conversation

@Tech-Psycho95
Copy link

@Tech-Psycho95 Tech-Psycho95 commented Mar 8, 2026

Summary

This PR addresses critical flaws in voting algorithms and restores security safeguards in the anonymous voting module. These fixes ensure election results are mathematically correct before being permanently stored on-chain, and restore the identity verification and security mechanisms that were previously bypassed.


🗳️ Voting Algorithm Fixes

Moore's Method (MooreResult.sol)

Bug: Majority threshold incorrectly used candidatesLength / 2 instead of totalVotes / 2

Fix: Now correctly calculates total votes and checks if any candidate exceeds 50% of actual votes cast.


Quadratic Voting (QuadraticBallot.sol)

Bug: Credit calculation was linear (subtracting votes) instead of quadratic (N votes should cost N² credits)

Fix: Properly implements quadratic cost calculation where allocating N votes to a candidate costs N² credits out of 100 total.


IRV - Instant Runoff Voting (IRVResult.sol, IRV.sol)

Bugs:

  1. Unbounded while(true) loop could cause DoS
  2. Majority calculation used total votes instead of active (non-exhausted) ballots
  3. Tie detection logic was flawed
  4. No validation of vote permutations

Fixes:

  • Capped iterations to numCandidates to prevent unbounded loops
  • Majority now calculated against active ballots only
  • Proper tie detection when all remaining candidates have equal votes
  • Added _validatePermutation() to ensure valid rankings

Kemeny-Young (KemenyYoungResult.sol, KemenyYoungBallot.sol)

Bug: Complex O(n²) pairwise scoring with incorrect winner determination

Fix: Simplified to Copeland pairwise comparison method (gas-efficient approximation). Each candidate scores +1 for each pairwise contest they win.


Vote Permutation Validation (Multiple Ballots)

Bug: Ranked voting ballots (IRV, Ranked, Schulze, KemenyYoung) did not validate that vote arrays were valid permutations

Fix: Added _validatePermutation() using bitmap to ensure:

  • No duplicate candidate rankings
  • All candidate IDs are within valid range

Added to: IRV.sol, RankedBallot.sol, SchulzeBallot.sol, KemenyYoungBallot.sol


Error Handling Improvements

Bug: BallotGenerator and ResultCalculator silently defaulted to GeneralBallot on invalid types

Fix: Now reverts with explicit InvalidBallotType and InvalidResultType errors


🔐 Anonymous Voting Module Security Restoration

Authentication Routing (App.js)

Bug: Authentication routing was commented out, allowing unauthenticated access

Fix: Re-enabled authentication check and redirect to /auth for unauthenticated users


Admin Verification (Election.js)

Bug: Admin check was hardcoded to true, bypassing authorization

Fix: Now validates connected wallet against electionOrganizer


Identity Verification Callback (PolygonIDVerifier.js)

Bug: Verification result callback was commented out

Fix: Re-enabled onVerificationResult(result) callback


Socket Session Isolation (index.js)

Bug: Socket events were broadcast to all connected clients instead of specific sessions

Fix: Changed io.sockets.emit() to io.to(sessionId).emit() for session-specific messaging


Memory Leak Prevention (index.js)

Bug: Auth requests stored in memory were never cleaned up

Fix: Added 10-minute cleanup timeout for auth requests


Issuer Validation (KYCAgeCredential.js)

Bug: allowedIssuers: ["*"] accepted credentials from any issuer

Fix: Set to empty array (reject by default) with TODO for production configuration


📜 Election Contract Hardening

Time Validation (Election.sol, ElectionFactory.sol)

  • Added InvalidElectionTime error
  • Elections must have startTime < endTime
  • Elections must have startTime > block.timestamp

Modifier Fix (Election.sol)

  • Renamed electionStartedelectionNotStarted
  • Fixed boundary: >= instead of > to prevent operations at exact start time

Remove Candidate Fix (Election.sol)

  • Now correctly updates moved candidate's ID when swapping with last element

Double-Result Prevention (Election.sol)

  • Added ResultsAlreadyDeclared error
  • Prevents calling getResult() multiple times

Ballot-Result Compatibility (ElectionFactory.sol)

  • Added _validateBallotResultCompat() to ensure only compatible ballot/result type pairs

🖥️ Client Fixes

Async Chain Switching Race Condition

Files: ButtonCard.tsx, CandidateCard.tsx, CandidateGrid.tsx, CreditsVoting.tsx, CrossChain.tsx

Bug: switchChain() was not awaited, causing contract calls to execute before chain switch completed

Fix: Changed to await switchChainAsync()


IPFS API Security

Bug: Pinata JWT was exposed client-side via NEXT_PUBLIC_PINATA_JWT

Fix:

  • Created server-side API route (/api/ipfs/route.ts)
  • Updated pinToIPFS.ts to proxy through server
  • JWT now stored as PINATA_JWT (server-only)

GetUserElections Hook

Bug: Returned all elections instead of filtering by connected user

Fix: Now fetches election owners and filters where owner === connectedAddress


SSR Compatibility

Bug: window.location.href accessed during server-side render

Fix: Added SSR guard and moved state update to useEffect


Files Changed (28 files, +368/-213 lines)

Blockchain (13 files)

  • blockchain/contracts/Election.sol
  • blockchain/contracts/ElectionFactory.sol
  • blockchain/contracts/ballots/BallotGenerator.sol
  • blockchain/contracts/ballots/IRV.sol
  • blockchain/contracts/ballots/KemenyYoungBallot.sol
  • blockchain/contracts/ballots/QuadraticBallot.sol
  • blockchain/contracts/ballots/RankedBallot.sol
  • blockchain/contracts/ballots/SchulzeBallot.sol
  • blockchain/contracts/ballots/interface/Errors.sol
  • blockchain/contracts/resultCalculators/IRVResult.sol
  • blockchain/contracts/resultCalculators/KemenyYoungResult.sol
  • blockchain/contracts/resultCalculators/MooreResult.sol
  • blockchain/contracts/resultCalculators/ResultCalculator.sol

Client (10 files)

  • client/app/api/ipfs/route.ts (new)
  • client/app/components/Cards/ButtonCard.tsx
  • client/app/components/Cards/VotingCards/CandidateCard.tsx
  • client/app/components/Cards/VotingCards/CandidateGrid.tsx
  • client/app/components/Cards/VotingCards/CreditsVoting.tsx
  • client/app/components/Helper/CrossChain.tsx
  • client/app/components/Hooks/GetUserElections.ts
  • client/app/election/[id]/page.tsx
  • client/app/helpers/client.ts
  • client/app/helpers/pinToIPFS.ts

Anonymous Voting (5 files)

  • anonymousVoting/clientAnonymousVoting/src/App.js
  • anonymousVoting/clientAnonymousVoting/src/components/Election.js
  • anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js
  • anonymousVoting/serverAnonymous/index.js
  • anonymousVoting/serverAnonymous/vcHelpers/KYCAgeCredential.js

⚠️ Breaking Changes

  1. Elections with startTime >= endTime or startTime <= block.timestamp will revert
  2. Invalid ballot/result type combinations will revert (no silent fallback)
  3. Quadratic voting credit calculation now correct (may affect existing vote patterns)
  4. Anonymous voting now requires proper authentication

Testing Checklist

  • Unit tests for Moore result with various vote distributions
  • Unit tests for Quadratic voting credit edge cases
  • Unit tests for IRV with exhausted ballots and ties
  • Unit tests for vote permutation validation
  • Integration tests for election time boundaries
  • E2E tests for anonymous voting authentication flow
  • E2E tests for client chain switching

Fixes Issue #237

Summary by CodeRabbit

Release Notes

  • New Features

    • Added IPFS file pinning and unpinning capabilities.
    • Added admin verification based on connected wallet.
    • Enhanced election validation (timing, candidate count, ballot type compatibility).
    • Added vote validation to prevent invalid selections.
    • Improved election result calculations across multiple voting systems.
  • Bug Fixes

    • Fixed network switching delays.
    • Fixed verification callback execution.
    • Prevented memory leaks in auth requests.
    • Strengthened election state management.
  • Chores

    • Improved security with server-side credential handling.

Voting Algorithm Fixes:
- Moore: fixed majority threshold (totalVotes/2 vs candidatesLength/2)
- Quadratic: fixed credit calculation to use N² cost per N votes
- IRV: cap iterations, fixed majority calc with active ballots, improved tie handling
- Kemeny-Young: simplified to Copeland pairwise method for gas efficiency
- Added vote permutation validation to IRV, Ranked, Schulze, KemenyYoung ballots
- Added InvalidBallotType/InvalidResultType errors instead of silent fallbacks

Election Contract Hardening:
- Added election time validation (start < end, start > now)
- Fixed removeCandidate to update moved candidate's ID
- Added ResultsAlreadyDeclared guard
- Fixed electionNotStarted modifier boundary check
- Added ballot-result type compatibility validation in factory

Anonymous Voting Security Restoration:
- Re-enabled authentication routing for unauthenticated users
- Fixed admin check to validate wallet against electionOrganizer
- Re-enabled identity verification callback in PolygonIDVerifier
- Changed socket emit from broadcast to session-specific
- Added auth request cleanup timeout to prevent memory leaks
- Set allowedIssuers to empty array (reject by default)

Client Fixes:
- Fixed race condition: switchChain -> await switchChainAsync
- Moved Pinata JWT to server-side API route
- Fixed GetUserElections to filter by connected user
- Fixed SSR compatibility for window.location access
@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

This pull request introduces comprehensive input validation across blockchain ballot types with permutation checking, enhances server-side socket.io communication with targeted emissions and memory cleanup, implements async chain switching in the client application, adds server-side IPFS proxying, strengthens election time and admin permission checks, and refactors result calculation logic for several voting systems.

Changes

Cohort / File(s) Summary
Anonymous Voting Client – Routing & Authentication
anonymousVoting/clientAnonymousVoting/src/App.js, anonymousVoting/clientAnonymousVoting/src/components/Election.js
Activated conditional routing based on registration status; replaced static admin assignment with dynamic Ethereum signer verification against election organizer address.
Anonymous Voting Client – Verification
anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js
Enabled onVerificationResult callback invocation to propagate verification outcome to parent component.
Anonymous Voting Server – Socket.io & Verification
anonymousVoting/serverAnonymous/index.js
Switched from broadcast to targeted socket emissions per session; added 10-minute auth request cleanup; improved error handling with JSON response format; made port configurable via environment.
Anonymous Voting Server – Credential Validation
anonymousVoting/serverAnonymous/vcHelpers/KYCAgeCredential.js
Restricted allowed issuers from permissive wildcard to empty array (requires production configuration).
Blockchain – Election Contract
blockchain/contracts/Election.sol
Added time validity enforcement (startTime < endTime and startTime >= current); introduced safe candidate removal by index; added ResultsAlreadyDeclared error; strengthened result declaration guards; consolidated permission checks.
Blockchain – Election Factory
blockchain/contracts/ElectionFactory.sol
Added election validation: enforce ≥2 candidates, time validity, and ballot-result type compatibility via new _validateBallotResultCompat helper.
Blockchain – Ballot & Result Validation
blockchain/contracts/ballots/BallotGenerator.sol, blockchain/contracts/ballots/Errors.sol
Replaced silent fallback with InvalidBallotType revert; added InvalidVotePermutation error to interface.
Blockchain – Ranked Ballot Contracts
blockchain/contracts/ballots/IRV.sol, blockchain/contracts/ballots/RankedBallot.sol, blockchain/contracts/ballots/SchulzeBallot.sol, blockchain/contracts/ballots/KemenyYoungBallot.sol
Added _validatePermutation helper to enforce valid permutations (no duplicates, all indices in range) before recording votes; IRV additionally added onlyOwner to getVotes.
Blockchain – Quadratic Ballot
blockchain/contracts/ballots/QuadraticBallot.sol
Replaced linear with quadratic credit cost: votes cost voteArr[i]² credits, total must be ≤100 and >0.
Blockchain – Result Calculators
blockchain/contracts/resultCalculators/IRVResult.sol
Added input validation (revert on empty votes); refactored elimination logic to detect ties, handle ballot removal correctly, and bound iterations by candidate count.
Blockchain – Result Calculators
blockchain/contracts/resultCalculators/KemenyYoungResult.sol
Refactored from full Kemeny-Young matrix to Copeland-style pairwise scoring approximation using scores array.
Blockchain – Result Calculators
blockchain/contracts/resultCalculators/MooreResult.sol
Added early winner detection when candidate exceeds totalVotes / 2; replaced candidate count threshold with vote-count-based threshold.
Blockchain – Result Calculator Router
blockchain/contracts/resultCalculators/ResultCalculator.sol
Removed default fallback; explicitly map result types (1–8) to calculation methods; revert with InvalidResultType on unknown types.
Client (Next.js) – IPFS Integration
client/app/api/ipfs/route.ts
Added new server route implementing Pinata IPFS pin/unpin via POST/DELETE with JWT validation and error handling.
Client (Next.js) – Chain Switching
client/app/components/Cards/ButtonCard.tsx, client/app/components/Cards/VotingCards/CandidateCard.tsx, client/app/components/Cards/VotingCards/CandidateGrid.tsx, client/app/components/Cards/VotingCards/CreditsVoting.tsx, client/app/components/Helper/CrossChain.tsx
Replaced synchronous switchChain with asynchronous switchChainAsync from useSwitchChain; updated call sites to await chain switch completion.
Client (Next.js) – Hooks & Pages
client/app/components/Hooks/GetUserElections.ts, client/app/election/[id]/page.tsx
Enhanced GetUserElections to fetch owners separately and filter elections by connected user; made ElectionPage SSR-safe by avoiding window access during server render and synchronizing data via useEffect.
Client (Next.js) – Configuration & Helpers
client/app/helpers/client.ts, client/app/helpers/pinToIPFS.ts
Removed polygonAmoy network; made WalletConnect projectId environment-driven; switched IPFS calls from direct Pinata API to server-proxied /api/ipfs endpoint; removed JWT from client-side code.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 The ballots now validate with care,
Permutations checked with logic so fair!
Sockets whisper to sessions alone,
IPFS pinned through routes newly shown—
A hopping great update, well-grown! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: fixing voting algorithms and restoring anonymous voting security, which are the core objectives of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
anonymousVoting/clientAnonymousVoting/src/App.js (1)

31-41: ⚠️ Potential issue | 🟡 Minor

Missing catch-all route for registered users.

When hasRegistered is true, there's no catch-all route (path="*"). Unmatched URLs will render nothing instead of redirecting to a known route.

🔧 Proposed fix
       <Routes>
         <Route path="/" element={<Navigate to="/auth" />} />
         <Route path="/auth" element={<Register/>} />
         <Route path="/voting/:id" element={<VotingPage/>} />
         <Route path="/createProcess" element={<CreateProcess/>} />
         <Route path="/dashboard" element={<Dashboard/>} />
         <Route path="/election" element={<Election />} />
+        <Route path="*" element={<Navigate to="/dashboard" />} />
       </Routes>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@anonymousVoting/clientAnonymousVoting/src/App.js` around lines 31 - 41, The
Routes block rendered when hasRegistered is true lacks a catch-all Route, so
unmatched URLs render nothing; update the Routes (the JSX that returns <Routes>
with <Route path="/" element={<Navigate to="/auth" />} /> and other Routes like
"/voting/:id", "/createProcess", "/dashboard", "/election") to include a
catch-all Route (path="*") that redirects to a valid route (e.g., Navigate to
"/dashboard" or "/auth") so any unknown path is handled and users are redirected
appropriately.
anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js (1)

53-65: ⚠️ Potential issue | 🟠 Major

Socket connection created on every render.

io(serverUrl) is called at module scope (line 53), which means a new socket connection is created on every component render. This can lead to multiple active connections and memory leaks.

Move the socket creation inside the useEffect and include cleanup:

🔧 Proposed fix
-  const socket = io(serverUrl);
-
-  useEffect(() => {
-    console.log('url', serverUrl)
-    socket.on("connect", () => {
-      setSessionId(socket.id);
-
-      // only watch this session's events
-      socket.on(socket.id, (arg) => {
-        setSocketEvents((socketEvents) => [...socketEvents, arg]);
-      });
-    });
-  }, []);
+  useEffect(() => {
+    console.log('url', serverUrl)
+    const socket = io(serverUrl);
+    
+    socket.on("connect", () => {
+      setSessionId(socket.id);
+
+      // only watch this session's events
+      socket.on(socket.id, (arg) => {
+        setSocketEvents((socketEvents) => [...socketEvents, arg]);
+      });
+    });
+    
+    return () => {
+      socket.disconnect();
+    };
+  }, [serverUrl]);

Note: This will also require adjusting the socket.close() call on line 95 to use a ref or state to track the socket instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js`
around lines 53 - 65, The socket is being created at module scope via
io(serverUrl) causing a new connection on every render; move the io(serverUrl)
call into the useEffect that registers listeners, create and assign the socket
to a ref (e.g., socketRef.current) or state, register the connect handler there
(calling setSessionId and socket.on(socket.id,... ) using socketRef.current),
and add a cleanup function that calls socketRef.current.disconnect() or
socketRef.current.close() to remove listeners and close the connection; also
update the existing socket.close() usage to reference the ref/state instance
instead of the module-scoped variable.
🧹 Nitpick comments (5)
client/app/components/Hooks/GetUserElections.ts (1)

26-28: Consider adding enabled guard to skip query when no elections exist.

When allElections is undefined or empty, useReadContracts is called with an empty contracts array. While wagmi handles this gracefully, adding an enabled option makes the intent explicit and avoids unnecessary query lifecycle transitions.

♻️ Suggested improvement
-  const { data: owners, isLoading: loadingOwners } = useReadContracts({
-    contracts: ownerCalls,
-  });
+  const { data: owners, isLoading: loadingOwners } = useReadContracts({
+    contracts: ownerCalls,
+    query: {
+      enabled: ownerCalls.length > 0,
+    },
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/components/Hooks/GetUserElections.ts` around lines 26 - 28, The
hook calls useReadContracts with ownerCalls even when allElections is
undefined/empty; add an enabled guard so the query is skipped when there are no
elections. Update the useReadContracts call in GetUserElections.ts to pass an
options object with enabled set to a boolean expression (e.g.,
Boolean(allElections?.length) or ownerCalls.length > 0) so the hook runs only
when ownerCalls contains contracts, leaving the existing destructuring of data
as-is.
anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js (1)

92-95: Use numeric timeout value.

The setTimeout delay is passed as a string "2000" instead of a number. While JavaScript coerces this, it's clearer to use a numeric literal.

✏️ Suggested fix
             setTimeout(() => {
               reportVerificationResult(true);
-            }, "2000");
+            }, 2000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js`
around lines 92 - 95, In PolygonIDVerifier update the setTimeout call that
currently uses the string "2000" to use a numeric timeout (2000) instead; locate
the setTimeout(...) that calls reportVerificationResult(true) and replace the
string literal with the number 2000 so the delay is passed as a numeric value,
leaving the surrounding logic (reportVerificationResult and socket.close())
unchanged.
anonymousVoting/clientAnonymousVoting/src/App.js (1)

34-34: Consider redirecting / to /dashboard for registered users.

Redirecting registered users from / to /auth seems counterintuitive. Typically, authenticated users landing on the root would be sent to a dashboard or home view rather than back to authentication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@anonymousVoting/clientAnonymousVoting/src/App.js` at line 34, The root Route
currently unconditionally navigates to /auth; change it to conditionally
redirect based on authentication state by checking the app's auth flag/hook
(e.g., isAuthenticated from your auth context or useAuth) and update the Route
in App.js so the element uses that flag to Navigate to /dashboard for
authenticated users and to /auth for unauthenticated users (or wrap with your
existing ProtectedRoute/RequireAuth component if present).
client/app/election/[id]/page.tsx (1)

2-4: Initialize currentUrl from pathname and update in a mount effect to prevent hydration mismatch.

The server renders pathname, but the client immediately switches to window.location.href. Since ClipBoard renders inputValue on line 16, this divergence causes different markup between server and client on first load. Initialize from pathname and fill in the full URL within a useEffect:

Suggested fix
-import React, { useEffect } from "react";
+import React, { useEffect, useState } from "react";
 import { useAccount } from "wagmi";
 import { usePathname } from "next/navigation";
@@ -16,7 +16,11 @@ const ElectionPage = ({ params }: { params: { id: `0x${string}` } }) => {
   const pathname = usePathname();
   const electionAddress = params.id;
   const { electionData, setelectionData } = useElectionData();
+  const [currentUrl, setCurrentUrl] = useState(pathname);
 
+  useEffect(() => {
+    setCurrentUrl(window.location.href);
+  }, [pathname]);
-  const currentUrl = typeof window !== "undefined" ? window.location.href : `${pathname}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/election/`[id]/page.tsx around lines 2 - 4, The component
currently sets currentUrl from window.location.href causing a hydration
mismatch; instead initialize currentUrl state from the server-provided pathname
(usePathname()) and then in a client-only useEffect update it to the full
window.location.href so ClipBoard's inputValue is identical on first render;
update the state initialization and add a useEffect that sets currentUrl =
window.location.href, referencing the currentUrl state, usePathname(), and the
ClipBoard/inputValue usage to locate the code to change.
blockchain/contracts/resultCalculators/ResultCalculator.sol (1)

19-41: Good improvement: Explicit revert on invalid result types.

Reverting with InvalidResultType() instead of silently defaulting to GeneralResult is the correct approach for fail-fast behavior and clearer error handling.

,

💡 Optional: Consolidate duplicate branches

Result types 1, 2, 5, and 6 all route to calculateGeneralResult. You could consolidate them:

-        if (_resultType == 1 || _resultType == 2) {
-            return calculateGeneralResult(returnData);
-        } else if (_resultType == 3) {
+        if (_resultType == 1 || _resultType == 2 || _resultType == 5 || _resultType == 6) {
+            return calculateGeneralResult(returnData);
+        } else if (_resultType == 3) {
             return calculateIRVResult(returnData);
         } else if (_resultType == 4) {
             return calculateSchulzeResult(returnData);
-        } else if (_resultType == 5) {
-            return calculateGeneralResult(returnData);
-        } else if (_resultType == 6) {
-            return calculateGeneralResult(returnData);
         } else if (_resultType == 7) {

However, keeping them separate may be intentional for future differentiation—skip if that's the case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blockchain/contracts/resultCalculators/ResultCalculator.sol` around lines 19
- 41, The getResults function duplicates branches for result types 1, 2, 5, and
6 that all call calculateGeneralResult; simplify by consolidating those branches
into a single conditional (e.g., if (_resultType == 1 || _resultType == 2 ||
_resultType == 5 || _resultType == 6) return
calculateGeneralResult(returnData);) while keeping the existing branches for
_resultType values 3, 4, 7, 8 and the explicit revert InvalidResultType()
unchanged; update getResults to use the consolidated condition to remove
redundancy but preserve the same behavior and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@anonymousVoting/clientAnonymousVoting/src/components/Election.js`:
- Around line 116-133: The useEffect's checkAdmin currently only sets
setAdmin(true) and never clears it; modify checkAdmin (used in the Election
component) so that after obtaining address and comparing with
electionDetails.electionOrganizer it explicitly calls setAdmin(false) when the
addresses don't match or when ethereum/electionDetails/electionOrganizer are
missing, and add an ethereum.on('accountsChanged', ...) listener inside the same
useEffect to re-run checkAdmin on account changes (ensure you remove the
listener in the cleanup function); reference the checkAdmin function, setAdmin
state setter, and the useEffect to implement these changes.

In `@anonymousVoting/serverAnonymous/vcHelpers/KYCAgeCredential.js`:
- Around line 8-9: The allowedIssuers array in KYCAgeCredential.js was set to an
empty array which rejects all issuers; update the module so production issuer
DIDs are loaded at runtime by either (a) populating the allowedIssuers constant
with the production issuer DID strings in KYCAgeCredential.js (reference symbol:
allowedIssuers in KYCAgeCredential.js) or (b) add environment variable support
(e.g., ALLOWED_ISSUERS) that the file parses into an array at startup and
assigns to allowedIssuers, validating it is non-empty and logging/throwing a
clear error if not configured; also update
anonymousVoting/serverAnonymous/README.md with instructions for setting allowed
issuers or the ALLOWED_ISSUERS env var and examples.

In `@blockchain/contracts/ballots/IRV.sol`:
- Around line 36-43: The bitmap duplicate-detection in _validatePermutation (and
equivalent validators in KemenyYoungBallot.sol, RankedBallot.sol,
SchulzeBallot.sol) breaks for candidate IDs >= 256 because EVM left-shift wraps
to zero; replace the bitmap approach with a dynamic bool[] seen = new bool[](n)
and then for each arr[i] assert arr[i] < n, if seen[arr[i]] revert
InvalidVotePermutation(), else set seen[arr[i]] = true, or alternatively enforce
a candidate count cap <= 256 at the election layer (Election.sol) if you prefer
keeping the bitmap—apply the same change consistently across the four ballot
contract functions.

In `@blockchain/contracts/ballots/KemenyYoungBallot.sol`:
- Around line 45-52: The permutation-validation bitmap in _validatePermutation
(KemenyYoungBallot.sol) is unsafe for n >= 256 because shifting a uint by >=256
bits invalidates duplicate detection; update the validation to either enforce an
explicit upper bound (e.g., require(n <= 256) with a clear error) or replace the
bitmap logic with a dynamic bool[] seen array and check/set seen[arr[i]] to
detect duplicates; apply the same fix to the other affected validators
(RankedBallot, SchulzeBallot, IRV ballot validation functions) so candidate
counts above 255 are correctly handled.

In `@blockchain/contracts/ballots/RankedBallot.sol`:
- Around line 41-48: The _validatePermutation function uses a single uint bitmap
(bitmap and 1 << arr[i]) which fails for candidate indices >=256; replace the
bitmap approach with a dynamic boolean array: allocate bool[] seen = new
bool[](n), check arr[i] < n, revert on seen[arr[i]] already true, and set
seen[arr[i]] = true to detect duplicates reliably; alternatively, if you intend
to keep the bitmap, enforce an explicit cap by reverting when n > 256 (but
prefer the bool[] seen approach for correctness).

In `@blockchain/contracts/ballots/SchulzeBallot.sol`:
- Around line 41-48: The _validatePermutation function currently uses a 256-bit
bitmap which breaks when totalCandidates > 256 (shifts overflow), so replace the
bitmap approach with a dynamic bool[] memory seen = new bool[](n) and use
seen[arr[i]] checks to detect duplicates; keep the existing bounds check (arr[i]
< n) and revert InvalidVotePermutation on out-of-range or if seen[arr[i]] is
already true, then set seen[arr[i]] = true; this change fixes the overflow
vulnerability in _validatePermutation and accommodates the fact addCandidate
enforces no upper bound on candidate count.

In `@blockchain/contracts/Election.sol`:
- Around line 138-147: The removeCandidate function currently allows the owner
to delete candidates down to zero; update removeCandidate to enforce a minimum
candidate count (e.g., require(candidates.length - 1 >= 2) or check after
removal) so that removing a candidate cannot reduce candidates below the
factory-enforced minimum of 2; add a new custom error declaration (e.g., error
MinimumCandidatesRequired(); ) and use it in removeCandidate (or a require with
a clear message) to revert when removal would violate the minimum; ensure the
check references the candidates array and is applied alongside the existing
swap-and-pop logic in removeCandidate and that the new error symbol is declared
at the top of the contract.

In `@blockchain/contracts/resultCalculators/KemenyYoungResult.sol`:
- Around line 22-25: The implementation of performKemenyYoung currently computes
Copeland winners rather than true Kemeny-Young, so update the contract to avoid
mislabeling: either implement the actual Kemeny-Young algorithm behind the
existing Kemeny-Young API or (preferable) rename/export this implementation as a
distinct Copeland method and add a new result type/value for Copeland;
specifically, create a new function name like performCopeland (or expose
performKemenyYoungAsCopeland) and add/modify the result enum/identifier used by
the contract to include COPLAND (or COPLAND_EQUIVALENT) while leaving
performKemenyYoung unchanged or implementing the correct Kemeny-Young
computation, and update all callers to reference the new function/name (search
for performKemenyYoung, Kemeny-Young result enums, and any consumers of the
result type to update usage).

In `@client/app/api/ipfs/route.ts`:
- Around line 13-26: The handler currently always returns
NextResponse.json(data) which yields HTTP 200 even when the Pinata fetch
response is 4xx/5xx; update the logic around the fetch response (the response
variable from the pinJSONToIPFS request) to check response.ok and only return
the success JSON when true, otherwise return the upstream status and error
payload by calling NextResponse.json with the error body and passing
response.status (and optional statusText) so the client sees the real Pinata
status; adjust the return paths that use response, data, and NextResponse.json
accordingly.
- Around line 39-46: The handler currently accepts any cid and always returns
success; update the logic around const { cid } = await request.json() to
validate that cid is a non-empty string (return a 400 NextResponse when
missing/invalid), then perform the fetch to
https://api.pinata.cloud/pinning/unpin/${cid} and check the upstream response;
only return success when response.ok is true, otherwise return a failure
NextResponse that forwards the upstream status and error body (or a useful error
message). Locate the cid extraction and the fetch call in route.ts to make these
changes.
- Around line 5-51: The /api/ipfs route lacks access control and doesn't
propagate upstream Pinata HTTP errors; add an auth check/middleware to restrict
access (e.g., verify a server-side token/session before invoking handlers) or
validate a request header within the exported POST and DELETE functions, and
keep the existing JWT guard; in both POST and DELETE handlers (functions POST
and DELETE) inspect the fetch response (response.ok / response.status) from
Pinata, parse the response body and return NextResponse.json with that body and
the same status code when non-2xx, and only return success when the upstream
response indicates success—do not always return { success: true } on DELETE and
ensure errors from fetch are logged and returned with a 500 if the request
failed locally.

In `@client/app/election/`[id]/page.tsx:
- Around line 25-34: The page can render stale electionData from the persistent
Zustand store while new electionInformation is still loading; update the sync to
prefer fresh data and clear/store-reset on election id changes: in the component
add electionAddress (or id) and address to the useEffect dependency array and
either 1) set a resolvedElectionData variable that prefers electionInformation
over electionData (e.g. resolvedElectionData = electionInformation ??
electionData) and use that for all downstream destructures, or 2) explicitly
clear the Zustand store (call setelectionData(null) or a reset method) when the
election id/address changes before fetching; ensure useEffect that calls
setelectionData references useElectionInformation/electionInformation,
electionAddress (id) and address so the store is updated deterministically.

In `@client/app/helpers/client.ts`:
- Line 9: The current config sets projectId:
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "" which silently allows an
empty projectId; instead validate NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID at load
time and throw a clear error if it's missing. Update the code that builds the
config (the object containing the projectId key in client/app/helpers/client.ts)
to read the env var into a local const (e.g., const projectId =
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID) and if (!projectId) throw new
Error("NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID must be set for WalletConnect");
then use that non-null projectId value in the exported config.

In `@client/app/helpers/pinToIPFS.ts`:
- Around line 6-13: The helper currently returns whatever JSON the /api/ipfs
endpoint yields even when the HTTP response is an error; update the pinToIPFS
function to check response.ok after the fetch and, if false, read the response
body (e.g., await response.text() or await response.json()) and throw an Error
(or a custom Error) containing the status and the error body so callers will
receive a thrown exception for 4xx/5xx responses instead of a success payload.
- Around line 22-26: The DELETE call in pinToIPFS.ts currently ignores the fetch
response so unpin failures appear as success; update the function that performs
the fetch (the code block sending method: "DELETE" with body JSON.stringify({
cid: CID })) to await the Response, check response.ok, and throw an Error
(including response.status and/or await response.text() for details) when
!response.ok so callers of pinToIPFS can handle or surface failures.

---

Outside diff comments:
In `@anonymousVoting/clientAnonymousVoting/src/App.js`:
- Around line 31-41: The Routes block rendered when hasRegistered is true lacks
a catch-all Route, so unmatched URLs render nothing; update the Routes (the JSX
that returns <Routes> with <Route path="/" element={<Navigate to="/auth" />} />
and other Routes like "/voting/:id", "/createProcess", "/dashboard",
"/election") to include a catch-all Route (path="*") that redirects to a valid
route (e.g., Navigate to "/dashboard" or "/auth") so any unknown path is handled
and users are redirected appropriately.

In
`@anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js`:
- Around line 53-65: The socket is being created at module scope via
io(serverUrl) causing a new connection on every render; move the io(serverUrl)
call into the useEffect that registers listeners, create and assign the socket
to a ref (e.g., socketRef.current) or state, register the connect handler there
(calling setSessionId and socket.on(socket.id,... ) using socketRef.current),
and add a cleanup function that calls socketRef.current.disconnect() or
socketRef.current.close() to remove listeners and close the connection; also
update the existing socket.close() usage to reference the ref/state instance
instead of the module-scoped variable.

---

Nitpick comments:
In `@anonymousVoting/clientAnonymousVoting/src/App.js`:
- Line 34: The root Route currently unconditionally navigates to /auth; change
it to conditionally redirect based on authentication state by checking the app's
auth flag/hook (e.g., isAuthenticated from your auth context or useAuth) and
update the Route in App.js so the element uses that flag to Navigate to
/dashboard for authenticated users and to /auth for unauthenticated users (or
wrap with your existing ProtectedRoute/RequireAuth component if present).

In
`@anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js`:
- Around line 92-95: In PolygonIDVerifier update the setTimeout call that
currently uses the string "2000" to use a numeric timeout (2000) instead; locate
the setTimeout(...) that calls reportVerificationResult(true) and replace the
string literal with the number 2000 so the delay is passed as a numeric value,
leaving the surrounding logic (reportVerificationResult and socket.close())
unchanged.

In `@blockchain/contracts/resultCalculators/ResultCalculator.sol`:
- Around line 19-41: The getResults function duplicates branches for result
types 1, 2, 5, and 6 that all call calculateGeneralResult; simplify by
consolidating those branches into a single conditional (e.g., if (_resultType ==
1 || _resultType == 2 || _resultType == 5 || _resultType == 6) return
calculateGeneralResult(returnData);) while keeping the existing branches for
_resultType values 3, 4, 7, 8 and the explicit revert InvalidResultType()
unchanged; update getResults to use the consolidated condition to remove
redundancy but preserve the same behavior and error handling.

In `@client/app/components/Hooks/GetUserElections.ts`:
- Around line 26-28: The hook calls useReadContracts with ownerCalls even when
allElections is undefined/empty; add an enabled guard so the query is skipped
when there are no elections. Update the useReadContracts call in
GetUserElections.ts to pass an options object with enabled set to a boolean
expression (e.g., Boolean(allElections?.length) or ownerCalls.length > 0) so the
hook runs only when ownerCalls contains contracts, leaving the existing
destructuring of data as-is.

In `@client/app/election/`[id]/page.tsx:
- Around line 2-4: The component currently sets currentUrl from
window.location.href causing a hydration mismatch; instead initialize currentUrl
state from the server-provided pathname (usePathname()) and then in a
client-only useEffect update it to the full window.location.href so ClipBoard's
inputValue is identical on first render; update the state initialization and add
a useEffect that sets currentUrl = window.location.href, referencing the
currentUrl state, usePathname(), and the ClipBoard/inputValue usage to locate
the code to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1745db4-a80f-4ef9-9611-b99dae784b8a

📥 Commits

Reviewing files that changed from the base of the PR and between e4df55f and a30944f.

📒 Files selected for processing (28)
  • anonymousVoting/clientAnonymousVoting/src/App.js
  • anonymousVoting/clientAnonymousVoting/src/components/Election.js
  • anonymousVoting/clientAnonymousVoting/src/components/modals/PolygonIDVerifier.js
  • anonymousVoting/serverAnonymous/index.js
  • anonymousVoting/serverAnonymous/vcHelpers/KYCAgeCredential.js
  • blockchain/contracts/Election.sol
  • blockchain/contracts/ElectionFactory.sol
  • blockchain/contracts/ballots/BallotGenerator.sol
  • blockchain/contracts/ballots/IRV.sol
  • blockchain/contracts/ballots/KemenyYoungBallot.sol
  • blockchain/contracts/ballots/QuadraticBallot.sol
  • blockchain/contracts/ballots/RankedBallot.sol
  • blockchain/contracts/ballots/SchulzeBallot.sol
  • blockchain/contracts/ballots/interface/Errors.sol
  • blockchain/contracts/resultCalculators/IRVResult.sol
  • blockchain/contracts/resultCalculators/KemenyYoungResult.sol
  • blockchain/contracts/resultCalculators/MooreResult.sol
  • blockchain/contracts/resultCalculators/ResultCalculator.sol
  • client/app/api/ipfs/route.ts
  • client/app/components/Cards/ButtonCard.tsx
  • client/app/components/Cards/VotingCards/CandidateCard.tsx
  • client/app/components/Cards/VotingCards/CandidateGrid.tsx
  • client/app/components/Cards/VotingCards/CreditsVoting.tsx
  • client/app/components/Helper/CrossChain.tsx
  • client/app/components/Hooks/GetUserElections.ts
  • client/app/election/[id]/page.tsx
  • client/app/helpers/client.ts
  • client/app/helpers/pinToIPFS.ts

Comment on lines 116 to +133
useEffect(() => {
if(1) {
setAdmin(true);
}
}, [])
const checkAdmin = async () => {
try {
const { ethereum } = window;
if (ethereum && electionDetails && electionDetails.electionOrganizer) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const address = await signer.getAddress();
if (address.toLowerCase() === electionDetails.electionOrganizer.toLowerCase()) {
setAdmin(true);
}
}
} catch (err) {
console.log(err);
}
};
checkAdmin();
}, [electionDetails])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Admin state never resets to false.

The checkAdmin effect only sets setAdmin(true) when addresses match but never resets to false when they don't. If the user switches wallets or disconnects, isAdmin remains true from a previous check.

🔧 Proposed fix
 	useEffect(() => {
 		const checkAdmin = async () => {
 			try {
 				const { ethereum } = window;
 				if (ethereum && electionDetails && electionDetails.electionOrganizer) {
 					const provider = new ethers.providers.Web3Provider(ethereum);
 					const signer = provider.getSigner();
 					const address = await signer.getAddress();
 					if (address.toLowerCase() === electionDetails.electionOrganizer.toLowerCase()) {
 						setAdmin(true);
-					}
+					} else {
+						setAdmin(false);
+					}
+				} else {
+					setAdmin(false);
 				}
 			} catch (err) {
 				console.log(err);
+				setAdmin(false);
 			}
 		};
 		checkAdmin();
 	}, [electionDetails])

Additionally, consider listening for account changes via ethereum.on('accountsChanged', ...) to re-check admin status when the user switches wallets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@anonymousVoting/clientAnonymousVoting/src/components/Election.js` around
lines 116 - 133, The useEffect's checkAdmin currently only sets setAdmin(true)
and never clears it; modify checkAdmin (used in the Election component) so that
after obtaining address and comparing with electionDetails.electionOrganizer it
explicitly calls setAdmin(false) when the addresses don't match or when
ethereum/electionDetails/electionOrganizer are missing, and add an
ethereum.on('accountsChanged', ...) listener inside the same useEffect to re-run
checkAdmin on account changes (ensure you remove the listener in the cleanup
function); reference the checkAdmin function, setAdmin state setter, and the
useEffect to implement these changes.

Comment on lines +8 to +9
// TODO: Configure production issuers. An empty array rejects all issuers by default.
allowedIssuers: [],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's any documentation or environment variable handling for allowed issuers
rg -n "allowedIssuers" --type js
rg -n "ALLOWED_ISSUERS" --type-add 'env:*.env*' -t env

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 160


🏁 Script executed:

cat -n anonymousVoting/serverAnonymous/vcHelpers/KYCAgeCredential.js

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 918


🏁 Script executed:

find . -type f \( -name "*deploy*" -o -name "*config*" -o -name "README*" \) | head -20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 588


🏁 Script executed:

rg -n "allowedIssuers" -B 5 -A 5 --type js

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1197


🏁 Script executed:

cat anonymousVoting/serverAnonymous/README.md

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 7440


🏁 Script executed:

rg -n "issuer" --type md

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 419


🏁 Script executed:

# Check how this credential helper is used in the codebase
rg -n "KYCAgeCredential" -B 2 -A 2 --type js

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 2618


Empty allowedIssuers must be configured before deployment.

Changing from ["*"] to [] correctly removes the insecure wildcard issuer, but an empty array rejects all credentials. The deployment documentation (anonymousVoting/serverAnonymous/README.md) does not include steps to configure allowed issuers. Add configuration instructions covering either:

  • How to update allowedIssuers array with production issuer DIDs
  • Environment variable support (e.g., ALLOWED_ISSUERS) to inject issuer configuration at runtime

Without this, the verification will fail for all users in production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@anonymousVoting/serverAnonymous/vcHelpers/KYCAgeCredential.js` around lines 8
- 9, The allowedIssuers array in KYCAgeCredential.js was set to an empty array
which rejects all issuers; update the module so production issuer DIDs are
loaded at runtime by either (a) populating the allowedIssuers constant with the
production issuer DID strings in KYCAgeCredential.js (reference symbol:
allowedIssuers in KYCAgeCredential.js) or (b) add environment variable support
(e.g., ALLOWED_ISSUERS) that the file parses into an array at startup and
assigns to allowedIssuers, validating it is non-empty and logging/throwing a
clear error if not configured; also update
anonymousVoting/serverAnonymous/README.md with instructions for setting allowed
issuers or the ALLOWED_ISSUERS env var and examples.

Comment on lines +36 to +43
function _validatePermutation(uint[] memory arr, uint n) internal pure {
uint bitmap = 0;
for (uint i = 0; i < n; i++) {
if (arr[i] >= n) revert InvalidVotePermutation();
uint bit = 1 << arr[i];
if (bitmap & bit != 0) revert InvalidVotePermutation();
bitmap |= bit;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "IRV.sol" -type f

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 109


🏁 Script executed:

find . -name "*Election*.sol" -type f | head -20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 149


🏁 Script executed:

find . -path "*/ballots/*.sol" -type f

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 562


🏁 Script executed:

cat -n ./blockchain/contracts/ballots/IRV.sol | head -100

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1656


🏁 Script executed:

wc -l ./blockchain/contracts/ballots/IRV.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 112


🏁 Script executed:

cat -n ./blockchain/contracts/Election.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 6473


🏁 Script executed:

wc -l ./blockchain/contracts/Election.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 110


🏁 Script executed:

grep -r "_validatePermutation" ./blockchain/contracts/ballots/ --include="*.sol"

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 980


🏁 Script executed:

grep -r "bitmap" ./blockchain/contracts/ballots/ --include="*.sol" -n

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1153


🏁 Script executed:

cat -n ./blockchain/contracts/ElectionFactory.sol | head -150

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 6630


🏁 Script executed:

find ./blockchain/test -name "*.test.js" -o -name "*.spec.js" | xargs grep -l "candidate\|bitmap\|validatePermutation" 2>/dev/null | head -10

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 514


🏁 Script executed:

find ./blockchain/test -name "*ballot*" -o -name "*Ballot*" | head -10

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 53


🏁 Script executed:

grep -n "candidate\|bitmap\|validatePermutation\|256" ./blockchain/test/unit/IRVVoting.test.js | head -20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 675


🏁 Script executed:

cat -n ./blockchain/test/unit/IRVVoting.test.js | head -80

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 3602


🏁 Script executed:

grep -n "256\|bit\|bitmap" ./blockchain/contracts/ballots/*.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 2497


🌐 Web query:

Solidity uint256 shift operator 1 << 256 behavior overflow wrap

💡 Result:

In Solidity on the EVM, uint256(1) << 256 evaluates to 0 (it does not wrap to 1, and it does not revert).

Why:

  • The EVM SHL opcode specifies: if the shift amount is ≥ 256, the result is 0. So shifting left by exactly 256 clears the value. [1]
  • Solidity shift operations never perform overflow checks; instead the result is always truncated to the left operand’s type (here uint256). So there’s no “checked overflow revert” behavior for <<. [2]

Implication: you cannot represent 2**256 in a uint256; 1 << 255 is the largest single-bit value representable, and 1 << 256 becomes 0. [1][2]

Sources: [1] EIP-145 (EVM SHL) https://eips.ethereum.org/EIPS/eip-145, [2] Solidity documentation (Shifts: truncation; no overflow checks) https://docs.soliditylang.org/ (PDF download)


Bitmap-based duplicate detection fails for candidate IDs ≥ 256.

In Solidity, 1 << arr[i] returns 0 when arr[i] >= 256 (EVM SHL opcode specification). This causes the duplicate check to silently pass for invalid permutations with IDs ≥ 256. Since Election.sol enforces no upper candidate limit, any caller can create elections with > 256 candidates and bypass validation.

This issue affects 4 ballot contracts: IRV.sol, KemenyYoungBallot.sol, RankedBallot.sol, and SchulzeBallot.sol. Either add an explicit candidate count cap (≤ 256) in the election layer or replace the bitmap with a bool[] seen array.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blockchain/contracts/ballots/IRV.sol` around lines 36 - 43, The bitmap
duplicate-detection in _validatePermutation (and equivalent validators in
KemenyYoungBallot.sol, RankedBallot.sol, SchulzeBallot.sol) breaks for candidate
IDs >= 256 because EVM left-shift wraps to zero; replace the bitmap approach
with a dynamic bool[] seen = new bool[](n) and then for each arr[i] assert
arr[i] < n, if seen[arr[i]] revert InvalidVotePermutation(), else set
seen[arr[i]] = true, or alternatively enforce a candidate count cap <= 256 at
the election layer (Election.sol) if you prefer keeping the bitmap—apply the
same change consistently across the four ballot contract functions.

Comment on lines +45 to +52
function _validatePermutation(uint[] memory arr, uint n) internal pure {
uint bitmap = 0;
for (uint i = 0; i < n; i++) {
if (arr[i] >= n) revert InvalidVotePermutation();
uint bit = 1 << arr[i];
if (bitmap & bit != 0) revert InvalidVotePermutation();
bitmap |= bit;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine KemenyYoungBallot.sol
fd KemenyYoungBallot.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 121


🏁 Script executed:

# Search for candidate count constraints and initialization
rg "candidateCount|numCandidates|candidate.*limit|MAX.*CANDIDATE" blockchain/contracts --type sol -A 3 -B 3

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 98


🏁 Script executed:

# Look for _validatePermutation usage and calls
rg "_validatePermutation" blockchain/ -A 5 -B 5

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 7349


🏁 Script executed:

# Check election layer for candidate count validation
rg "createElection|initializeElection|initialize" blockchain/contracts --type sol -A 10 -B 2

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 98


🏁 Script executed:

# Read KemenyYoungBallot.sol to see initialization and full context
cat blockchain/contracts/ballots/KemenyYoungBallot.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1718


🏁 Script executed:

# Search for ballot initialization/constructor patterns
grep -r "constructor\|initialize" blockchain/contracts/ballots/ --include="*.sol" -A 10

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 6322


🏁 Script executed:

# Look for candidate count constraints or constants
grep -r "MAX\|LIMIT\|256\|candidates" blockchain/contracts --include="*.sol" -B 2 -A 2

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 31688


🏁 Script executed:

# Check test files for permutation validation tests
find blockchain/test -name "*.test.js" -o -name "*.spec.js" | head -20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 534


🏁 Script executed:

# Check test files for KemenyYoung to see what candidate counts are tested
cat blockchain/test/unit/KementYoungVoting.test.js | head -100

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 3712


🏁 Script executed:

# Search for any max candidate constraints or limits in tests
rg "candidate|total.*candidate" blockchain/test/ -B 2 -A 2 | head -100

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 7268


🏁 Script executed:

# Verify EVM shift behavior: what happens with 1 << 256 in Solidity
cat > /tmp/test_shift.sol << 'EOF'
pragma solidity ^0.8.24;

contract ShiftTest {
    function testShift() public pure returns (uint) {
        uint bit256 = 1 << 256;  // Should be 0
        return bit256;
    }
    
    function testShift255() public pure returns (uint) {
        uint bit255 = 1 << 255;  // Should be valid
        return bit255;
    }
}
EOF
cat /tmp/test_shift.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 393


🏁 Script executed:

# Check ElectionFactory for candidate count validation/limits
cat blockchain/contracts/ElectionFactory.sol | grep -A 20 "createElection"

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 929


🏁 Script executed:

# Look for any documentation or comments about candidate count limits
rg "256|limit|max|candidate.*cap" blockchain/contracts --type sol -B 2 -A 2

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 98


🏁 Script executed:

# Check if there are tests that attempt to use large candidate counts
rg "candidate.*[0-9]{3,}" blockchain/test --include="*.js" -B 2 -A 2

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 512


🏁 Script executed:

# Search for large candidate counts in tests without type filter
grep -r "candidate" blockchain/test --include="*.js" | grep -E "[0-9]{2,}" | head -20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 373


🏁 Script executed:

# Check ElectionFactory.sol fully for candidate limits
cat blockchain/contracts/ElectionFactory.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 5935


🏁 Script executed:

# Look at what Solidity version comment says about shifts >= 256
grep -r "shift\|<<\|>>" blockchain/contracts --include="*.sol" -B 3 -A 3

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 2414


🏁 Script executed:

# Check if there's any documentation about candidate limits in README or similar
find blockchain -name "README*" -o -name "*.md" | xargs cat 2>/dev/null

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1113


_validatePermutation bitmap only works reliably for candidate counts up to 255.

In Solidity, shifting a uint256 by >= 256 bits wraps around or produces zero. If an election were created with 256+ candidates, the duplicate-detection bitmap would fail—candidate IDs >= 256 would not map to unique bits, allowing invalid permutations to pass validation. Since ElectionFactory.sol imposes no candidate count limit (only checking minimum ≥ 2), this creates an unprotected edge case. Add an explicit cap on candidate count (e.g., require n <= 256 in _validatePermutation or at election initialization), or replace the bitmap with a bool[] seen array.

This pattern appears in KemenyYoungBallot, RankedBallot, SchulzeBallot, and IRV ballots.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blockchain/contracts/ballots/KemenyYoungBallot.sol` around lines 45 - 52, The
permutation-validation bitmap in _validatePermutation (KemenyYoungBallot.sol) is
unsafe for n >= 256 because shifting a uint by >=256 bits invalidates duplicate
detection; update the validation to either enforce an explicit upper bound
(e.g., require(n <= 256) with a clear error) or replace the bitmap logic with a
dynamic bool[] seen array and check/set seen[arr[i]] to detect duplicates; apply
the same fix to the other affected validators (RankedBallot, SchulzeBallot, IRV
ballot validation functions) so candidate counts above 255 are correctly
handled.

Comment on lines +41 to +48
function _validatePermutation(uint[] memory arr, uint n) internal pure {
uint bitmap = 0;
for (uint i = 0; i < n; i++) {
if (arr[i] >= n) revert InvalidVotePermutation();
uint bit = 1 << arr[i];
if (bitmap & bit != 0) revert InvalidVotePermutation();
bitmap |= bit;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the RankedBallot.sol file
find . -name "RankedBallot.sol" -type f

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 118


🏁 Script executed:

# Check the actual implementation of _validatePermutation and surrounding context
cat -n blockchain/contracts/ballots/RankedBallot.sol | head -80

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1975


🏁 Script executed:

# Search for where _validatePermutation is called to understand what values of n are used
rg "_validatePermutation" --type solidity -B 2 -A 2

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 103


🏁 Script executed:

# Search for any constraints on candidate count in the election layer
rg "candidates|candidateCount|maxCandidates" blockchain/contracts --type solidity -A 1 -B 1

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 103


🏁 Script executed:

# Check if there are any tests that verify the candidate count limits
rg "_validatePermutation|candidate.*256|bitmap" blockchain/test --type javascript

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 105


🏁 Script executed:

# Search for where RankedBallot.init is called (Election contract)
rg "init\(" blockchain/contracts --type-list | head -20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 460


🏁 Script executed:

# Try again without type specification
rg "\.init\(" blockchain/contracts -A 2 -B 2

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 791


🏁 Script executed:

# Search for Election contract that uses RankedBallot
rg "RankedBallot" blockchain/contracts

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 362


🏁 Script executed:

# Look for any candidate count or limit constants
rg "Candidate|candidate|MAX_" blockchain/contracts/*.sol

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 2354


🏁 Script executed:

# Check test files for how init is called (use pattern matching)
find blockchain/test -name "*.test.js" -o -name "*.spec.js" | xargs grep -l "init\|RankedBallot" 2>/dev/null | head -5

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 275


🏁 Script executed:

# Check the actual Election.sol file to understand the candidates array and any limits
cat -n blockchain/contracts/Election.sol | grep -A 20 "Candidate\[\]"

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1906


🏁 Script executed:

# Look at test files for typical candidate counts
grep -r "candidates\|totalCandidate" blockchain/test/*.js | grep -E "length|count|= [0-9]" | head -20

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 53


🏁 Script executed:

# Check if there's documentation or constraints in comments
grep -B 5 -A 5 "candidates" blockchain/contracts/Election.sol | head -40

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 1315


🏁 Script executed:

# Look at specific test for RankedBallot voting
find blockchain/test -name "*.js" -exec grep -l "RankedBallot\|_validatePermutation" {} \;

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 53


🏁 Script executed:

# Search for test initialization patterns with specific candidate counts
rg "new Election\|createElection\|candidates.*length|totalCandidate" blockchain/test --context 3

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 53


🏁 Script executed:

# Look at IRVVoting test which likely tests RankedBallot
cat blockchain/test/unit/IRVVoting.test.js | head -150

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 5882


🏁 Script executed:

# Check lifecycle tests for candidate setup
find blockchain/test/lifecycle -name "*.test.js" -exec head -100 {} \;

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 131


_validatePermutation is only correct up to 256 candidates.

This bitmap-based duplicate check relies on a single uint. In Solidity 0.8.24, the shift operator 1 << arr[i] wraps when arr[i] >= 256, causing the duplicate detection to fail silently. Since the election layer does not enforce n <= 256, invalid permutations with candidate IDs >= 256 can be accepted. Either add an explicit cap at the election layer or replace the bitmap with a bool[] seen array.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blockchain/contracts/ballots/RankedBallot.sol` around lines 41 - 48, The
_validatePermutation function uses a single uint bitmap (bitmap and 1 << arr[i])
which fails for candidate indices >=256; replace the bitmap approach with a
dynamic boolean array: allocate bool[] seen = new bool[](n), check arr[i] < n,
revert on seen[arr[i]] already true, and set seen[arr[i]] = true to detect
duplicates reliably; alternatively, if you intend to keep the bitmap, enforce an
explicit cap by reverting when n > 256 (but prefer the bool[] seen approach for
correctness).

Comment on lines +39 to +46
const { cid } = await request.json();

await fetch(`https://api.pinata.cloud/pinning/unpin/${cid}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${JWT}` },
});

return NextResponse.json({ success: true });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t acknowledge unpins that Pinata rejected.

This handler never validates cid and ignores the upstream status, so /unpin/undefined or any Pinata 4xx/5xx still returns { success: true }. Reject empty CIDs and only return success when response.ok is true.

Suggested fix
     try {
         const { cid } = await request.json();
+        if (!cid || typeof cid !== 'string') {
+            return NextResponse.json({ error: 'cid is required' }, { status: 400 });
+        }

-        await fetch(`https://api.pinata.cloud/pinning/unpin/${cid}`, {
+        const response = await fetch(`https://api.pinata.cloud/pinning/unpin/${cid}`, {
             method: 'DELETE',
             headers: { Authorization: `Bearer ${JWT}` },
         });
 
-        return NextResponse.json({ success: true });
+        if (!response.ok) {
+            return NextResponse.json({ error: 'Failed to unpin from IPFS' }, { status: response.status });
+        }
+
+        return NextResponse.json({ success: true });
     } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/api/ipfs/route.ts` around lines 39 - 46, The handler currently
accepts any cid and always returns success; update the logic around const { cid
} = await request.json() to validate that cid is a non-empty string (return a
400 NextResponse when missing/invalid), then perform the fetch to
https://api.pinata.cloud/pinning/unpin/${cid} and check the upstream response;
only return success when response.ok is true, otherwise return a failure
NextResponse that forwards the upstream status and error body (or a useful error
message). Locate the cid extraction and the fetch call in route.ts to make these
changes.

Comment on lines +25 to 34
// Use useEffect to update state to avoid setting state during render
useEffect(() => {
if (electionInformation) {
setelectionData(electionInformation);
}
}, [electionInformation, setelectionData]);

if (electionData !== electionInformation) {
setelectionData(electionInformation);
}
if (isLoading) return <Loader />;

if (!electionData) return <Loader />;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect whether `useElectionData` is shared/persistent and whether
# `useElectionInformation` returns fresh data independently of that store.
fd 'ElectionInfo.*' client/app/hooks -x sh -c 'echo "== $1 =="; sed -n "1,220p" "$1"' sh {}
fd 'GetElectionInformation.*' client/app/components/Hooks -x sh -c 'echo "== $1 =="; sed -n "1,220p" "$1"' sh {}

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 2131


🏁 Script executed:

cat -n client/app/election/[id]/page.tsx | head -100

Repository: AOSSIE-Org/Agora-Blockchain

Length of output: 4370


Avoid rendering stale election data when navigating between elections.

When switching to a different election, useElectionInformation fetches fresh data asynchronously while useElectionData (a persistent Zustand store) still holds the previous election's information. If isLoading becomes false before the useEffect syncs the new data, the page renders with the wrong owner/candidates/actions. Additionally, the effect's dependency array (line 30) omits electionAddress and address, making the synchronization implicit rather than explicit.

Prefer rendering from fresh data by checking electionInformation first, or clear the store when the election ID changes. The suggested refactor below addresses both timing and clarity:

Suggested refactor
   useEffect(() => {
     if (electionInformation) {
       setelectionData(electionInformation);
     }
-  }, [electionInformation, setelectionData]);
+  }, [electionInformation, setelectionData, electionAddress, address]);

-  if (isLoading) return <Loader />;
+  const resolvedElectionData = electionInformation ?? electionData;
+
+  if (isLoading || !resolvedElectionData) return <Loader />;

Then update all electionData[n] destructures (lines 35–44) to use resolvedElectionData[n].

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Use useEffect to update state to avoid setting state during render
useEffect(() => {
if (electionInformation) {
setelectionData(electionInformation);
}
}, [electionInformation, setelectionData]);
if (electionData !== electionInformation) {
setelectionData(electionInformation);
}
if (isLoading) return <Loader />;
if (!electionData) return <Loader />;
// Use useEffect to update state to avoid setting state during render
useEffect(() => {
if (electionInformation) {
setelectionData(electionInformation);
}
}, [electionInformation, setelectionData, electionAddress, address]);
const resolvedElectionData = electionInformation ?? electionData;
if (isLoading || !resolvedElectionData) return <Loader />;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/election/`[id]/page.tsx around lines 25 - 34, The page can render
stale electionData from the persistent Zustand store while new
electionInformation is still loading; update the sync to prefer fresh data and
clear/store-reset on election id changes: in the component add electionAddress
(or id) and address to the useEffect dependency array and either 1) set a
resolvedElectionData variable that prefers electionInformation over electionData
(e.g. resolvedElectionData = electionInformation ?? electionData) and use that
for all downstream destructures, or 2) explicitly clear the Zustand store (call
setelectionData(null) or a reset method) when the election id/address changes
before fetching; ensure useEffect that calls setelectionData references
useElectionInformation/electionInformation, electionAddress (id) and address so
the store is updated deterministically.

export const config = getDefaultConfig({
appName: "Agora-Blockchain",
projectId: "8501447cf73c4e68061f7ed912d6a8ee",
projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For @rainbow-me/rainbowkit2.1.2 / WalletConnect integration, isprojectId required to be non-empty during initialization, and do the official docs recommend failing fast when the environment variable is missing?

💡 Result:

Yes—if you’re using WalletConnect via RainbowKit (including @rainbow-me/rainbowkit@2.1.2), projectId is expected to be provided (i.e., a real, non-empty WalletConnect/Reown Cloud Project ID). RainbowKit’s configuration APIs and examples treat projectId as a required input (they show it as a mandatory field and commonly wire it from an env var with a non-null assertion). [2]

On “fail fast”: RainbowKit’s own docs/examples don’t explicitly say “throw if the env var is missing”; they mostly rely on TypeScript patterns like process.env.X! / “YOUR_PROJECT_ID” placeholders rather than a runtime guard. [2] However, WalletConnect/Reown’s official React installation docs do explicitly recommend a runtime check and show throwing an error when projectId is missing (if (!projectId) { throw new Error('Project ID is not defined') }). [1]

Sources: [1] WalletConnect/Reown App SDK React installation docs, “Cloud Configuration” example with explicit throw guard; [2] RainbowKit configuration docs/examples showing projectId as required input (and using process.env...!).


Fail fast when NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID is missing.

Line 9 falls back to "" when the env var is absent. projectId must be non-empty for WalletConnect initialization, and the official WalletConnect/Reown setup guidance explicitly recommends throwing an error when it is missing. The empty string fallback masks a deployment misconfiguration and causes failures later at runtime.

Suggested change
+const walletConnectProjectId =
+  process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID;
+
+if (!walletConnectProjectId) {
+  throw new Error("Missing NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID");
+}
+
 export const config = getDefaultConfig({
   appName: "Agora-Blockchain",
-  projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "",
+  projectId: walletConnectProjectId,
   chains: [sepolia, avalancheFuji],
   ssr: true,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "",
const walletConnectProjectId =
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID;
if (!walletConnectProjectId) {
throw new Error("Missing NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID");
}
export const config = getDefaultConfig({
appName: "Agora-Blockchain",
projectId: walletConnectProjectId,
chains: [sepolia, avalancheFuji],
ssr: true,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/helpers/client.ts` at line 9, The current config sets projectId:
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "" which silently allows an
empty projectId; instead validate NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID at load
time and throw a clear error if it's missing. Update the code that builds the
config (the object containing the projectId key in client/app/helpers/client.ts)
to read the env var into a local const (e.g., const projectId =
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID) and if (!projectId) throw new
Error("NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID must be set for WalletConnect");
then use that non-null projectId value in the exported config.

Comment on lines +6 to 13
const response = await fetch("/api/ipfs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});

const data = await response.json();
console.log(data);
return data;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Throw when the pin proxy responds with an error.

fetch only rejects on transport failures. A 4xx/5xx from /api/ipfs still resolves here, so callers can continue with an error payload as if pinning succeeded.

Suggested fix
   try {
     const response = await fetch("/api/ipfs", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify(body),
     });
 
     const data = await response.json();
+    if (!response.ok) {
+      throw new Error(data?.error ?? "Failed to pin JSON to IPFS");
+    }
     return data;
   } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/helpers/pinToIPFS.ts` around lines 6 - 13, The helper currently
returns whatever JSON the /api/ipfs endpoint yields even when the HTTP response
is an error; update the pinToIPFS function to check response.ok after the fetch
and, if false, read the response body (e.g., await response.text() or await
response.json()) and throw an Error (or a custom Error) containing the status
and the error body so callers will receive a thrown exception for 4xx/5xx
responses instead of a success payload.

Comment on lines +22 to +26
await fetch("/api/ipfs", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cid: CID }),
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate unpin failures to callers.

This helper ignores the HTTP status completely, so failed deletes resolve as success. Read the response and throw on !response.ok so the caller can recover or notify the user.

Suggested fix
   try {
-    await fetch("/api/ipfs", {
+    const response = await fetch("/api/ipfs", {
       method: "DELETE",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify({ cid: CID }),
     });
+
+    if (!response.ok) {
+      const data = await response.json().catch(() => null);
+      throw new Error(data?.error ?? "Failed to unpin JSON from IPFS");
+    }
   } catch (err) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await fetch("/api/ipfs", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cid: CID }),
});
try {
const response = await fetch("/api/ipfs", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cid: CID }),
});
if (!response.ok) {
const data = await response.json().catch(() => null);
throw new Error(data?.error ?? "Failed to unpin JSON from IPFS");
}
} catch (err) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/helpers/pinToIPFS.ts` around lines 22 - 26, The DELETE call in
pinToIPFS.ts currently ignores the fetch response so unpin failures appear as
success; update the function that performs the fetch (the code block sending
method: "DELETE" with body JSON.stringify({ cid: CID })) to await the Response,
check response.ok, and throw an Error (including response.status and/or await
response.text() for details) when !response.ok so callers of pinToIPFS can
handle or surface failures.

@ArshLabs
Copy link

ArshLabs commented Mar 14, 2026

good work fixing the QuadraticBallot and RankedBallot logic
went through the contracts alongside this and noticed a few things not covered here yet:

  1. Election.sol still has zero emit statements across all state-changing functions (userVote, ccipVote, addCandidate, removeCandidate, getResult). the analytics dashboard in the GSoC roadmap needs indexed event logs to exist at all. opened a separate PR for this (feat: add event emissions to Election contract #250) if useful to reference.

  2. deleteElection() in ElectionFactory does the swap-and-pop correctly but electionCount only ever increments, never decrements on delete. after any deletion electionCount is permanently off by one.

  3. CCIPSender.sol still has gasLimit: 200_000 hardcoded. complex ballot types like Schulze need more gas for cross-chain votes and will silently fail on destination chain.

not blocking the PR just flagging for completeness. happy to open PRs for any of these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants