Skip to content

feat(torn): per-account voting power + Vault lock custody (gap #6)#2007

Closed
Zeugh-eth wants to merge 3 commits into
feat/tornado-cash-integrationfrom
feat/torn-voting-power
Closed

feat(torn): per-account voting power + Vault lock custody (gap #6)#2007
Zeugh-eth wants to merge 3 commits into
feat/tornado-cash-integrationfrom
feat/torn-voting-power

Conversation

@Zeugh-eth

Copy link
Copy Markdown
Member

What

Makes TORN voting power real (per-account) and fixes locked-supply accounting — the central data gaps for the holders/delegates and attack-profitability views. Targets feat/tornado-cash-integration.

Built per the dao-integration / anticapture-indexer skills: surgical, additive, deterministic, no hot-path RPC calls.

1. Per-account voting power (was missing)

TORN voting power = lockedBalance in the governor; there is no DelegateVotesChanged. We now derive each account's voting power from lock/unlock TORN Transfers:

  • New lockedVotingPowerChanged handler (eventHandlers/delegation.ts) populates accountPower.votingPower + votingPowerHistory — mirrors delegatedVotesChanged.
  • Previously delegates/holders showed 0 voting power.

2. Vault lock custody — gap #6 (correctness)

Lock detection treated only the governor as custody, but post-v2 lock routes TORN to the TornadoVault (GovernanceVaultUpgrade._transferTokens). We now treat governor + Vault as lock sinks (ignoring internal governor↔vault moves like the v2 migration). This recovers ~2.6M TORN of locked supply the governor-only logic missed (verified on-chain: governor 4.73M + vault 2.59M).

Verification

  • On-chain ground truth validated with the reconciliation smoke test (params, quorum semantics, custody split). After deploying this branch, run it in full mode (API_BASE=...) and require 0 FAIL — specifically delegatedSupply ≈ Σ lockedBalance and sampled accountPower.votingPower == lockedBalance(account).
  • ⚠️ Requires a full reindex to take effect (per the indexer skill's reindex warning).
  • I could not run the indexer/typecheck in my environment — please run pnpm --filter=@anticapture/indexer typecheck && lint and a backfill.

Deferred (notes in INTEGRATION.md)

  • Re-vote tally driftVoted uses onConflictDoNothing (avoids Ponder's batch-flush crash); a correct re-vote fix must reverse+reapply while staying replay-idempotent and needs an indexer test. Re-votes appear infrequent.
  • Exact voting power via RewardUpdate* + readContract — only if a Transfer-derived discrepancy is found.
  • Staking rewards, vote-extension tracking, proposal-target detector (feat(torn): proposal-target verification — flag malicious/look-alike execution targets #2005) — out of scope for the simple version.

Relationship to other PRs

Tornado voting power = lockedBalance (no DelegateVotesChanged). Derive it
from lock/unlock TORN Transfers in erc20.ts:
- Treat governor (pre-v2) AND TornadoVault (post-v2) as lock sinks; the
  non-sink side is the user whose voting power changes. Fixes delegatedSupply
  which previously tracked governor-only and missed ~2.6M TORN in the Vault.
- New lockedVotingPowerChanged handler populates accountPower.votingPower +
  votingPowerHistory (mirrors delegatedVotesChanged); deterministic, no
  hot-path RPC calls (indexer-skill safe).

Requires a full reindex. Re-vote tally fix deferred (needs indexer test) —
see INTEGRATION.md. Verify with the reconciliation smoke test.
@railway-app

railway-app Bot commented Jun 30, 2026

Copy link
Copy Markdown

This PR was not deployed automatically as @Zeugh-eth does not have access to the Railway project.

In order to get automatic PR deploys, please add @Zeugh-eth to your workspace on Railway.

@vercel

vercel Bot commented Jun 30, 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 Jun 30, 2026 12:54am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
anticapture Ignored Ignored Jun 30, 2026 12:54am

Request Review

TreasuryAddresses[TORN] is empty; governor commingles DAO treasury with
pre-v2 user locks so its balance isn't a clean treasury figure. Document
the correct approach + flag to confirm attack-profitability uses the
on-chain liquid-treasury call vs the indexed metric.

@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: f317b16d39

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/indexer/src/indexer/torn/erc20.ts Outdated
// into either is a lock; out of either is an unlock. Pre-v2 used the governor.
// https://etherscan.io/address/0x2F50508a8a3D323B91336FA3eA6Ae50e55f32185
const vaultAddress = getAddress(
"0x2F50508a8a3D323B91336FA3eA6Ae50e55f32185",

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 Use the valid checksum for the vault address

When DAO_ID=TORN, TORNTokenIndexer evaluates this getAddress() during handler registration. This mixed-case literal has an invalid checksum (the repository's existing vault spelling is ...A6ae50E55...), and viem rejects invalid checksummed addresses, so the TORN indexer fails before it can start. Reuse the existing constant or correct the checksum before passing it to getAddress.

Useful? React with 👍 / 👎.

@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: 8848fae7fe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +180 to +184
if (fromIsSink && !toIsSink) {
await updateDelegatedSupply(context, daoId, address, -value, timestamp);
await lockedVotingPowerChanged(context, daoId, {
account: to,
delta: -value,

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 Don't classify every custody outflow as an unlock

When a proposal transfers TORN out of the governor/vault, this is not necessarily a user unlock: Tornado proposals execute by delegatecall and can move treasury/attack-recovery funds, and the 2023 exploit also changed locked balances in storage before draining custody. In those cases this branch debits the recipient with delta: -value even though the recipient was never credited by a lock Transfer, so a full TORN reindex can write negative accountPower.votingPower and corrupt active-supply/history views. Key this off an actual lockedBalance change or governance event/read rather than raw Transfer direction.

Useful? React with 👍 / 👎.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants