Skip to content

Gate rounds voting by delegated Gnars votes#115

Open
xSatori wants to merge 1 commit into
r4topunk:mainfrom
xSatori:feat/rounds-voting-power
Open

Gate rounds voting by delegated Gnars votes#115
xSatori wants to merge 1 commit into
r4topunk:mainfrom
xSatori:feat/rounds-voting-power

Conversation

@xSatori

@xSatori xSatori commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

What this addresses

This PR upgrades rounds voting so future round votes are gated by delegated Gnars DAO governance voting power. A connected wallet now needs at least 1 delegated Gnars DAO vote from the Gnars token contract before it can vote in any rounds strategy.

Code to review

  • src/services/round-voting-power.ts

    • Adds the server-only delegated voting-power helper.
    • Reads DAO_ADDRESSES.token with serverPublicClient.readContract.
    • Uses getVotes(address) rather than balanceOf.
    • Validates and normalizes wallet addresses with Viem.
    • Returns 0 on RPC/read failure so voting is blocked instead of granted.
  • src/services/rounds.ts

    • Updates getRoundVotingPower to gate every strategy behind delegated Gnars votes.
    • one_per_wallet returns 1 only when delegated voting power is positive.
    • fixed_per_wallet returns round.votesPerWallet only when delegated voting power is positive.
    • one_per_nft now returns delegated governance voting power.
  • src/app/api/rounds/[slug]/voting-power/route.ts

    • Adds GET /api/rounds/[slug]/voting-power?wallet=....
    • Validates slug and wallet.
    • Returns walletAddress, votingPower, usedVotes, and remainingVotes.
  • src/components/rounds/RoundDetailView.tsx

    • Fetches voting power while voting is open and a wallet is connected.
    • Uses remainingVotes for allocation limits and submit enablement.
    • Shows clear copy when the wallet has no delegated Gnars voting power.
    • Hides voting controls for wallets with 0 voting power.
    • Refetches voting power and refreshes the route after successful submit.
  • Regression coverage

    • src/services/round-voting-power.test.ts
    • src/app/api/rounds/[slug]/voting-power/route.test.ts
    • src/components/rounds/RoundDetailView.test.tsx

Test plan

  • pnpm test
    • 11 test files passed
    • 86 tests passed
    • 1 todo
  • pnpm lint
    • passed with 0 errors
    • existing warnings remain for <img> usage outside the rounds files

Review notes

This branch was rebuilt from current upstream/main so the PR only contains the delegated-vote gating change. The main behavior to review is the mapping between delegated governance votes and each rounds voting strategy, especially one_per_nft, which now uses delegated Gnars DAO voting power directly instead of the previous safe one-wallet fallback.

@vercel

vercel Bot commented Jun 5, 2026

Copy link
Copy Markdown

@xSatori is attempting to deploy a commit to the r4to's projects Team on Vercel.

A member of the Team first needs to authorize it.

@r4topunk

r4topunk commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Automated multi-agent review — request changes

Reviewed in an isolated worktree by parallel Opus reviewers, then each finding was adversarially re-verified. Three findings confirmed; the voting-address mismatch blocks merge.

🔴 High — voting is impossible for external-wallet Gnars holders

The UI gates on useUserAddress().address (src/components/rounds/RoundDetailView.tsx:71), which for external wallets (MetaMask/Coinbase) defaults to the EOA — where Gnars are delegated — so controls enable and show "N of N votes remaining". But submitVotes signs with the smart account (RoundDetailView.tsx:126), and the server re-checks getRoundVotingPower against that SA address, which returns 0. Result: a real Gnars holder sees an enabled ballot, submits, and gets "This wallet does not have votes for this round."

Fix: gate on the same address that signs/enforces. Either read voting power for the SA when the vote will be signed by the SA, or pass the signing address through consistently (client check ↔ submitVotes signer ↔ server castRoundVotes must all use one address).

🟠 Medium — one_per_nft uses live votes, not a start-of-round snapshot

getRoundVotingPower (src/services/rounds.ts:471) returns live getVotes(wallet) at cast time. A holder can vote from wallet A, re-delegate/transfer Gnars to wallet B, and vote again with the same underlying tokens. The rest of the codebase's governance pre-checks use getPastVotes at a fixed block for exactly this reason. Recommend snapshotting delegated power at round start (store the block number on the round and read getPastVotes).

🟡 Low — race on view-mode toggle

fetchVotingPower (RoundDetailView.tsx:93) has no AbortController or sequence guard. Toggling EOA↔SA quickly (a supported action via WalletDrawer) can let a stale response overwrite current state, showing the wrong wallet's power. Add an ignore/abort cleanup in the effect.

Not a blocker (pre-existing, not this PR): the rounds feature has no next-intl scaffolding, so the new English strings match the existing (un-i18n'd) component — worth a separate i18n pass.

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