Skip to content

Fix Uniswap offchain proposal status - #2091

Merged
pikonha merged 10 commits into
devfrom
fix/uniswap-proposal-status
Jul 29, 2026
Merged

Fix Uniswap offchain proposal status#2091
pikonha merged 10 commits into
devfrom
fix/uniswap-proposal-status

Conversation

@pikonha

@pikonha pikonha commented Jul 29, 2026

Copy link
Copy Markdown
Member

Note

Medium Risk
Requires a DB migration and a long-running backfill; status display logic changes affect all offchain governance UI but is well covered by unit tests.

Overview
Fixes incorrect Snapshot proposal labels (e.g. Uniswap) by persisting quorum and scores total from Snapshot and using them when deriving dashboard status.

The offchain indexer and API now store and expose scoresTotal and quorum (DB migration, GraphQL fields, API schema). A separate metadata backfill loop re-fetches existing proposals by id in batches so historical rows get quorum data without resetting proposal or vote sync cursors.

getOffchainProposalStatus now treats active proposals as ongoing only before end; after end it resolves outcome. For basic proposals it returns no quorum when scoresTotal is below quorum, succeeded (not executed) when for wins after quorum, and defeated when against wins. List and detail UI pass the new fields into that helper.

Reviewed by Cursor Bugbot for commit 6aed140. Configure here.

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
anticapture-storybook Ready Ready Preview, Comment Jul 29, 2026 6:01pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
anticapture Ignored Ignored Jul 29, 2026 6:01pm

Request Review

@pikonha pikonha changed the title Fix Uniswap proposal status Fix Uniswap offchain proposal status Jul 29, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Backfill skips missing proposals
    • The backfill cursor now advances only through batch IDs that Snapshot actually returned, so missing proposals are retried instead of being skipped.

Create PR

Or push these changes by commenting:

@cursor push f6b0d9c708
Preview (f6b0d9c708)
diff --git a/apps/offchain-indexer/src/indexer.test.ts b/apps/offchain-indexer/src/indexer.test.ts
--- a/apps/offchain-indexer/src/indexer.test.ts
+++ b/apps/offchain-indexer/src/indexer.test.ts
@@ -220,6 +220,39 @@
     void promise;
   });
 
+  it("should not advance metadata backfill cursor past missing snapshot proposals", async () => {
+    const repo = createSimpleRepository();
+    repo.metadataBackfillIds.push("p-old", "p-missing");
+    const hydratedProposal = makeProposal({
+      id: "p-old",
+      created: 1700000000,
+      scores: [5_347_713.99, 0, 1_813.59],
+      scoresTotal: 5_349_527,
+      quorum: 10_000_000,
+    });
+    const provider = createSimpleProvider({
+      proposalsById: [hydratedProposal],
+    });
+    const indexer = new Indexer(repo, provider, 60_000);
+
+    const promise = indexer.start(false);
+    await vi.advanceTimersByTimeAsync(0);
+
+    expect(provider.fetchProposalsByIds).toHaveBeenCalledWith([
+      "p-old",
+      "p-missing",
+    ]);
+    expect(repo.saveProposalMetadataBackfill).toHaveBeenCalledWith(
+      [hydratedProposal],
+      "1700000000:p-old",
+    );
+    expect(repo.cursors.get("proposal_metadata_backfill")).toBe(
+      "1700000000:p-old",
+    );
+
+    void promise;
+  });
+
   it("should reset cursors when forceBackfill is true", async () => {
     const repo = createSimpleRepository();
     repo.cursors.set("proposals", "1700000000");

diff --git a/apps/offchain-indexer/src/indexer.ts b/apps/offchain-indexer/src/indexer.ts
--- a/apps/offchain-indexer/src/indexer.ts
+++ b/apps/offchain-indexer/src/indexer.ts
@@ -135,11 +135,36 @@
     if (!nextCursor) return;
 
     const proposals = await this.provider.fetchProposalsByIds(ids);
-    await this.repository.saveProposalMetadataBackfill(proposals, nextCursor);
-    this.proposalMetadataBackfillCursor = nextCursor;
+    const returnedIds = new Set(proposals.map((p) => p.id));
 
+    let cursor = this.proposalMetadataBackfillCursor;
+    for (const id of ids) {
+      if (!returnedIds.has(id)) break;
+      const proposal = proposals.find((p) => p.id === id)!;
+      cursor = `${proposal.created}:${id}`;
+    }
+    if (ids.every((id) => returnedIds.has(id))) {
+      cursor = nextCursor;
+    }
+
+    if (
+      cursor === this.proposalMetadataBackfillCursor &&
+      proposals.length === 0
+    ) {
+      return;
+    }
+
+    const cursorForSave = cursor ?? this.proposalMetadataBackfillCursor ?? "0:";
+    await this.repository.saveProposalMetadataBackfill(
+      proposals,
+      cursorForSave,
+    );
+    if (cursor !== this.proposalMetadataBackfillCursor) {
+      this.proposalMetadataBackfillCursor = cursor;
+    }
+
     logger.info(
-      { count: proposals.length, scanned: ids.length, cursor: nextCursor },
+      { count: proposals.length, scanned: ids.length, cursor: cursorForSave },
       "backfilled proposal metadata",
     );
   }

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 6aed140. Configure here.

Comment thread apps/offchain-indexer/src/indexer.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6aed140737

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/dashboard/features/governance/utils/offchainProposal.ts Outdated

Copy link
Copy Markdown
Collaborator

🎨 UI Review

Automated review · Figma: 🗳️ Off-chain Proposals (Voting + Visualization) — status taxonomy frame (found via ClickUp task DEV-1131) · Spec: 🗳️ Off-chain Proposals (Voting + Visualization)
ℹ️ Preview unreachable for this PR — the anticapture dashboard Vercel deployment is Ignored (only anticapture-storybook built). Review is grounded in the diff cross-checked against the Figma status-taxonomy frame (node 3536:26205), which was successfully opened.


Proposal list & detail — Off-chain (Snapshot) status badge

Must-fix — passed Snapshot proposals now show "Pending Queue" instead of a Passed label
getOffchainProposalStatus (apps/dashboard/features/governance/utils/offchainProposal.ts:17-18) changes the for-wins branch from ProposalStatus.EXECUTED to ProposalStatus.SUCCEEDED. SUCCEEDED's copy is defined once, shared with on-chain governor proposals, in getStatusText (apps/dashboard/features/governance/components/proposal-overview/ProposalItem.tsx:142-143): case ProposalStatus.SUCCEEDED: return "Pending Queue"; — and ProposalItem.tsx renders that text directly for off-chain proposals with no override. A Snapshot proposal never enters an on-chain queue step, so a passed vote will now read "Pending Queue", which is just as wrong as the "Executed" label this PR sets out to fix. Figma's status taxonomy (node 3536:26205) calls for a distinct green "Passed" badge for exactly this condition (basic · quorum met · For > Against → PASSED). Needs its own off-chain-specific label rather than reusing the on-chain SUCCEEDED copy. [Figma-confirmed]

Must-fix / question for author — below-quorum proposals surface a "No Quorum" badge that doesn't exist in the design
The new quorum check (offchainProposal.ts:14-16) returns ProposalStatus.NO_QUORUM when scoresTotal < quorum, which renders as a gray "No Quorum" pill (text-secondary/bg-secondary, ProposalItem.tsx:59/88/117/146). Figma's taxonomy (node 3536:26205) has exactly five badges — Pending / Active / Passed / Rejected / Closed — with no "No Quorum" badge, and its derivation table is explicit: basic · below quorum → REJECTED (the same red badge as a losing vote). Worth confirming with design whether reusing the on-chain NO_QUORUM status for Snapshot votes is an intentional new treatment, or whether it should resolve to the existing Rejected/Defeated badge per the spec. [Figma-confirmed]

Nice-to-have — rejection-quorum inversion not implemented
Figma's derivation table also has basic · rejection-quorum exceeded → REJECTED (inverted test) for Snapshot spaces configured with a rejection-type quorum. This PR's quorum check applies uniformly (scoresTotal < quorum → NO_QUORUM/rejected) and doesn't invert for that case. The parent spec (DEV-1131) already tracks "mirror snapshot.box's quorum-aware passed/rejected logic … including rejection quorum type" as an open engineering gap, so this is likely deliberately out of scope for this incremental fix — flagging so it isn't lost rather than as a blocker. [Figma-confirmed]

Validated against Figma — no change needed

  • Treating state === "active" as ongoing only while end >= now (not indefinitely) matches the design's "voting open → ACTIVE" rule tying status to time, not just Snapshot's raw state string.
  • Non-basic vote types (approval/multiple/ranked/weighted) resolving to CLOSED once voting ends matches "approval / multiple / ranked / weighted · ended → CLOSED — winner: X" — this PR doesn't add the "winner: X · NN%" copy suffix, but that's part of a separate, not-yet-built results-card scope in DEV-1131, not a regression introduced here.

Mobile

No mobile-specific surface changed — the status text/color functions are shared across breakpoints, so the two must-fix findings above apply equally on mobile.

Scope note

This is a UI-only review (visual fidelity, DS/design adherence, UX, copy) — the DB migration, backfill loop, and other backend/code-quality concerns are for the code reviewer.


Generated by Claude Code

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🔍 Vercel preview: https://anticapture-7tpyn63jy-ful.vercel.app

@railway-app

railway-app Bot commented Jul 29, 2026

Copy link
Copy Markdown

🚅 Deployed to the anticapture-pr-2091 environment in anticapture-infra

Service Status Web Updated (UTC)
ens-indexer-offchain ✅ Success (View Logs) Jul 29, 2026 at 6:19 pm
compound-indexer-offchain ✅ Success (View Logs) Jul 29, 2026 at 6:19 pm
gitcoin-indexer-offchain ✅ Success (View Logs) Jul 29, 2026 at 6:19 pm
shutter-indexer-offchain ✅ Success (View Logs) Jul 29, 2026 at 6:19 pm
uniswap-indexer-offchain ✅ Success (View Logs) Jul 29, 2026 at 6:19 pm
otelcol ✅ Success (View Logs) Jul 29, 2026 at 6:00 pm
tempo ✅ Success (View Logs) Jul 29, 2026 at 6:00 pm
gateful ✅ Success (View Logs) Web Jul 29, 2026 at 6:00 pm
alertmanager ✅ Success (View Logs) Web Jul 29, 2026 at 5:59 pm
authful ✅ Success (View Logs) Web Jul 29, 2026 at 5:59 pm
grafana ✅ Success (View Logs) Web Jul 29, 2026 at 5:59 pm
prometheus ✅ Success (View Logs) Jul 29, 2026 at 5:59 pm
loki ✅ Success (View Logs) Jul 29, 2026 at 5:59 pm
docs ✅ Success (View Logs) Web Jul 29, 2026 at 12:32 pm
mcp ✅ Success (View Logs) Web Jul 29, 2026 at 12:32 pm
compound-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
address-enrichment ✅ Success (View Logs) Web Jul 29, 2026 at 12:32 pm
gitcoin-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
nouns-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
ens-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
fluid-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
shutter-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
uniswap-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
lil-nouns-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
ens-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
aave-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
fluid-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
compound-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
tornado-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
nouns-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
shutter-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
obol-api ✅ Success (View Logs) Jul 29, 2026 at 12:32 pm
gitcoin-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
lil-nouns-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
uniswap-api ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
aave-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
tornado-api ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
scroll-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
obol-indexer ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
ens-relayer ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
erpc ✅ Success (View Logs) Web Jul 29, 2026 at 12:31 pm
nodeful ✅ Success (View Logs) Jul 29, 2026 at 12:31 pm
scroll-api ✅ Success (View Logs) Jul 29, 2026 at 12:29 pm
user-api ✅ Success (View Logs) Web Jul 29, 2026 at 12:28 pm

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44b492d702

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/offchain-indexer/src/indexer.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f95c5923df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -0,0 +1,2 @@
ALTER TABLE "snapshot"."proposals" ADD COLUMN "scores_total" double precision DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "snapshot"."proposals" ADD COLUMN "quorum" double precision DEFAULT 0 NOT NULL; No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve unknown quorum until backfill completes

On upgrades with existing proposals, this migration stores 0 as every row's quorum before the metadata backfill hydrates it. getOffchainProposalStatus interprets zero as having no quorum requirement and labels a basic proposal Passed whenever For exceeds Against, so the motivating 5.35M-of-10M Uniswap proposal is itself misclassified until processed. Because backfillProposalMetadata handles only 100 oldest-first rows per polling interval—and may repeatedly fail—incorrect statuses can persist for many cycles or indefinitely; represent unbackfilled quorum as unknown and gate outcome derivation on hydration, or complete the backfill before exposing the new status logic.

Useful? React with 👍 / 👎.

pikonha added 2 commits July 29, 2026 14:51
Returning DEFEATED conflated "nobody showed up" with "voted down" -
the enum and full ProposalItem styling for NO_QUORUM already exist.
Every caller of getProposalStatus/getProposalState feeds it the
onchain API's proposal.status, which never emits "passed" - only the
offchain path constructs ProposalStatus.PASSED directly.
pikonha added 3 commits July 29, 2026 14:51
The quorum row in ProposalInfoSection never appeared because the
Snapshot proposal was adapted with a hardcoded quorum of "0", even
though the API now indexes and exposes the real value.
The reconcile comment implied deletion was bounded to the 14-day
window, but backfillProposalMetadata walks and can delete proposals
across all history. Document that explicitly instead of hiding it
behind the word "backfill".
"Succeeded" was wrong - the code returns PASSED specifically to avoid
the on-chain "Pending Queue"/"succeeded" semantics for Snapshot
proposals.
@railway-app
railway-app Bot temporarily deployed to anticapture-infra / anticapture-pr-2091 July 29, 2026 17:59 Destroyed
@pikonha
pikonha merged commit 6b63c61 into dev Jul 29, 2026
57 checks passed
@pikonha
pikonha deleted the fix/uniswap-proposal-status branch July 29, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants