Fix Uniswap offchain proposal status - #2091
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
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.
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.
There was a problem hiding this comment.
💡 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".
🎨 UI Review
Proposal list & detail — Off-chain (Snapshot) status badgeMust-fix — passed Snapshot proposals now show "Pending Queue" instead of a Passed label Must-fix / question for author — below-quorum proposals surface a "No Quorum" badge that doesn't exist in the design Nice-to-have — rejection-quorum inversion not implemented Validated against Figma — no change needed
MobileNo 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 noteThis 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 |
|
🔍 Vercel preview: https://anticapture-7tpyn63jy-ful.vercel.app |
|
🚅 Deployed to the anticapture-pr-2091 environment in anticapture-infra
|
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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 | |||
There was a problem hiding this comment.
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 👍 / 👎.
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.
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.


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
scoresTotalandquorum(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.getOffchainProposalStatusnow treats active proposals as ongoing only beforeend; after end it resolves outcome. For basic proposals it returns no quorum whenscoresTotalis 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.