Skip to content
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
451db65
feat: integrate tornado cash
pikonha Jun 28, 2026
c9f71b5
Merge branch 'dev' into feat/tornado-cash-integration
pikonha Jun 29, 2026
f542b75
chore: enalbe offchain api returning 400 for the not-supporte ones
pikonha Jun 29, 2026
2a69466
fix(torn): correct quorum calc + governance-config facts (review foll…
Zeugh-eth Jun 29, 2026
4e60b18
fix(torn): reconcile dashboard with quorum + re-vote changes (Codex)
Zeugh-eth Jun 29, 2026
d94e2a0
fix: vp based on custody transfers
pikonha Jun 30, 2026
cbd82c1
feat(torn): custom-governor write actions — vote + delegate
Zeugh-eth Jun 30, 2026
3086165
fix(indexer): add torn treasury address
pikonha Jun 30, 2026
5cc2176
fix(indexer): title and description parsing
pikonha Jun 30, 2026
db66cd3
fix(dashboard): overview resiliance and attack exposure individually …
pikonha Jun 30, 2026
d8cdb61
chore: setup memory as postinstall
pikonha Jun 30, 2026
257f918
chore(dashboard): panel min height 400px
pikonha Jun 30, 2026
a6694d9
Merge pull request #2006 from blockful/fix/torn-review-quickfixes
pikonha Jun 30, 2026
3031315
feat(tornado-governance-page): first draft
PedroBinotto Jun 30, 2026
f1ae0fb
Merge branch 'feat/tornado-cash-integration' into feat/tornado-govern…
PedroBinotto Jun 30, 2026
00f4c26
feat(tornado-governance-page): action tab design adjustments
PedroBinotto Jun 30, 2026
1c07ed3
Merge branch 'feat/tornado-governance' of github.com:blockful/anticap…
PedroBinotto Jun 30, 2026
01a29aa
chore(tornado-governance-page): add tests to utils + id concatenation
PedroBinotto Jun 30, 2026
4ff84b4
fix: remove id concatenation
PedroBinotto Jun 30, 2026
4d96c51
fix: revert scripts alteration
PedroBinotto Jun 30, 2026
2c71302
fix: revert scripts alteration
PedroBinotto Jun 30, 2026
c4bc69a
Merge pull request #2012 from blockful/feat/tornado-governance
pikonha Jun 30, 2026
36992d7
chore: changeset
pikonha Jun 30, 2026
032a710
Merge pull request #2009 from blockful/feat/torn-write-actions
pikonha Jun 30, 2026
236fe26
Merge branch 'dev' into feat/tornado-cash-integration
pikonha Jun 30, 2026
593fc57
fix(api): tron governor added to non-circulating
pikonha Jun 30, 2026
1735924
chore: trigger deploy
pikonha Jun 30, 2026
203ea72
chore: disable postinstall
pikonha Jun 30, 2026
cdb00d2
test(api): offchain controllers 400
pikonha Jun 30, 2026
2f0aca6
fix(dashboard): delegation amount being correctly parsed
pikonha Jun 30, 2026
2fc7174
fix(api): add torn voting power repository
pikonha Jun 30, 2026
0d2da65
feat(indexer): compute delegation form governor
pikonha Jun 30, 2026
96c0f22
chore: enable preview url on the pr
pikonha Jun 30, 2026
412b9e8
fix(dashboard): allow vote change and disable abstain option
pikonha Jun 30, 2026
3129964
fix(dashboard): uppercase dao id on route redirect
pikonha Jul 1, 2026
6671b7c
chore: add local dev skill and --rw-api dev script flag
pikonha Jul 1, 2026
27aa217
chore(dashboard): remove attack exposure from torn
pikonha Jul 1, 2026
1454329
fix(indexer): handle governor vault transactions aggregation to deleg…
pikonha Jul 1, 2026
1a91eb5
fix(indexer): handle torn vote mutability
pikonha Jul 1, 2026
258eaef
chore(dashboard): change delegated to locked lable
pikonha Jul 1, 2026
d3dbc1e
chore: add contents read permission to tests gh action
pikonha Jul 1, 2026
7923fb5
fix(dashboard): torn delegate button gate
pikonha Jul 1, 2026
e2202b4
feat(dashboard): torn vote on proposal with delegated tokens
pikonha Jul 1, 2026
1877182
feat(dashboard): enable activity feed for torn
pikonha Jul 1, 2026
e8cd7ac
fix(dashboard): vote w/o delegator
pikonha Jul 1, 2026
732e60b
fix(indexer): use locked balance for TORN delegated value when zero
pikonha Jul 1, 2026
937c42f
fix(dashboard): render proposals relying on tx hash
pikonha Jul 1, 2026
873bb45
fix(api): rearrange torn transfer from and to
pikonha Jul 1, 2026
bb0a8f0
refactor(indexer): move torn governor from non-circulating to treasury
pikonha Jul 1, 2026
6c4fd5b
chore(dashboard): renamed delegated to blocked on torn tokens
pikonha Jul 1, 2026
2eed136
fix(indexer): exclude torn governance locks from treasury metric
pikonha Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/late-eagles-pick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/dashboard": minor
---

support Tornado Cash proposal creation
5 changes: 5 additions & 0 deletions .changeset/offchain-support-guards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/api": patch
---

Return unsupported-offchain errors consistently across offchain proposal and vote routes.
5 changes: 5 additions & 0 deletions .changeset/torn-delegation-voting-power.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/indexer": minor
---

TORN: route locked voting power to the locker's delegate and move it between delegates on Delegated/Undelegated. TORN emits no DelegateVotesChanged, so per-account voting power is now fully synthesized from lock/unlock Transfers plus delegation shifts.
5 changes: 5 additions & 0 deletions .changeset/torn-lock-transfer-direction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/api": patch
---

Normalize TORN lock/unlock transfer direction in voting-power history so the locker (not the custody contract) is shown as the delegator.
5 changes: 5 additions & 0 deletions .changeset/torn-proposal-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/dashboard": minor
---

Render Tornado Cash proposal descriptions as Markdown (unwrapping the stringified-JSON body) and show a proposal Info card on the Actions tab for proposals without executable actions.
5 changes: 5 additions & 0 deletions .changeset/torn-treasury-transfers-phantom-voting-power.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/indexer": patch
---

Fix TORN voting power going negative from treasury transfers. TORN derives per-account voting power from lock/unlock Transfers in/out of the governor, but TORN also flows through the governor for treasury purposes (proposal executions, batch grant payouts, the governor↔vault migration). Those were misread as user unlocks and subtracted voting power from recipients that never locked (e.g. proposal #6 funding a staking pool booked a -120k unlock against a contract with zero locked balance), producing negative voting power and phantom locked balances. A custody transfer is now only counted as a lock/unlock when its counterparty is the transaction sender, which is true for genuine `lock()`/`unlock()` calls and false for treasury flows.
5 changes: 5 additions & 0 deletions .changeset/torn-vote-change-mutability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/indexer": patch
---

Fix TORN votes not reflecting vote changes. TORN's governor lets a voter re-cast to change their vote, but the `Voted` handler used `onConflictDoNothing` on the unique `(voter, proposal)` row and dropped every subsequent cast — so a voter who switched from For to Against still showed as For and the proposal tally kept the stale vote. The handler now overwrites the existing row and moves the voting power between the for/against buckets on a change, while still treating an exact re-processed log (backfill/reorg) as a no-op. Requires a reindex to repopulate.
5 changes: 5 additions & 0 deletions .changeset/torn-vote-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/dashboard": minor
---

Make TORN vote recasting reachable (show "Change your vote" on already-voted onchain proposals when the DAO allows changing votes) and hide the Abstain option for Tornado Cash, whose binary governor rejects abstain votes.
6 changes: 6 additions & 0 deletions .changeset/torn-voting-power-history-transfer-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@anticapture/api": patch
"@anticapture/dashboard": patch
---

Fix TORN historical voting power rows being rendered as bogus zero-address delegations. TORN derives voting power directly from Transfers, so each history row shares the Transfer's log index, which the generic repository's strict `<` join never matched. Added a dedicated TORN voting-power repository that links the causing event at `logIndex <= row logIndex`. Dashboard also formats the auto-delegation fallback amount instead of dumping the raw delta.
7 changes: 7 additions & 0 deletions .changeset/tornado-cash-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@anticapture/indexer": minor
"@anticapture/api": minor
"@anticapture/dashboard": minor
---

Integrate Tornado Cash DAO (TORN): custom stake-to-vote indexer (lock-based delegated supply, timestamp governance), timestamp-based proposal-status API client, and dashboard config/icon.
61 changes: 61 additions & 0 deletions .claude/skills/local-dev-stack/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
name: local-dev
description: Use when you need to hit the running local stack (API, gateful, dashboard, indexer) to test or verify a change. Covers scripts/dev.sh, hot reload, and the ports each service listens on.
---

# Local Dev Stack

The local stack is started with `scripts/dev.sh` (usually already running — check the
ports before starting your own). **All services hot-reload on file save** — after
editing source you do NOT need to restart; just re-hit the endpoint.

## Start it

```bash
./scripts/dev.sh <dao> # e.g. tornado, uniswap, gitcoin, scroll, shutter, compound
./scripts/dev.sh <dao> --indexer # also run the indexer for that DAO
./scripts/dev.sh <dao> --debug-api # wait for API from IDE debugger instead of spawning it
./scripts/dev.sh # no DAO → API skipped, uses DAO_API_* from .env
```

### Railway env (per service)

By **default all services run locally** (env from `.env`). Opt into `railway run -e dev`
env injection per service:

| Flag | API | Gateful |
| -------------- | ------- | ------- |
| _(none)_ | local | local |
| `--rw-api` | railway | local |
| `--rw-gateful` | local | railway |
| `--rw` | railway | railway |

`--rw-api` falls back to `.env` if the Railway service isn't found. Relayer and
Address Enrichment always use `railway run` when available.

On startup it **kills anything already on the ports below**, so a second run replaces
the first.

## Ports

| Service | Port | URL | Notes |
| ------------------ | ----- | ---------------------- | ----------------------------------- |
| API | 42069 | http://localhost:42069 | only when a `<dao>` is passed |
| Indexer | 42070 | http://localhost:42070 | only with `--indexer` |
| Gateful (gateway) | 4001 | http://localhost:4001 | always |
| Dashboard | 3000 | http://localhost:3000 | always |
| Address Enrichment | 3001 | http://localhost:3001 | optional, skipped if no Railway svc |
| Relayer (ENS) | 3002 | http://localhost:3002 | ENS only |

Client SDK codegen runs in watch mode (no port) and regenerates on API/gateful changes.

## Testing tips

- **Test the API directly** at `http://localhost:42069` — OpenAPI/REST.
- **Test the aggregated surface** through gateful at `http://localhost:4001`.
- The DAO is identified by short id in env: `DAO_API_<UPPER_ID>` (e.g. `DAO_API_TORN`,
`DAO_API_UNI`). dev.sh exports these automatically.
- When the API restarts, a watchdog touches `apps/gateful/src/_dev-reload.ts` so gateful
reloads and re-points at the recovered API.

Stop everything with Ctrl+C (dev.sh traps it and kills the whole process group).
19 changes: 17 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ jobs:
needs: [configure-vercel-preview, wait-for-gateful]
if: needs.configure-vercel-preview.outputs.configured == 'true'
runs-on: ubuntu-latest
permissions:
pull-requests: write
Comment thread
pikonha marked this conversation as resolved.
contents: read

steps:
- name: Checkout repository
Expand All @@ -157,6 +160,7 @@ jobs:
run: echo "name=$(git log -1 --format='%an')" >> "$GITHUB_OUTPUT"

- name: Trigger Vercel preview redeploy
id: deploy
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_TEAM_ID }}
Expand All @@ -181,8 +185,11 @@ jobs:
# AND a runtime env (--env): the /api/gateful/* proxy route reads
# process.env.ANTICAPTURE_API_URL at request time, so --build-env alone
# would build against the right URL but serve a stale/missing one.
# `vercel deploy` prints the deployment URL to stdout; capture it so the
# step below can surface it on the PR (the CLI deploy path doesn't get
# Vercel's native Git-integration check/comment).
run: |
npx --yes vercel@latest deploy --yes --token="${VERCEL_TOKEN}" \
url=$(npx --yes vercel@latest deploy --yes --token="${VERCEL_TOKEN}" \
--build-env ANTICAPTURE_API_URL="${GATEFUL_URL}" \
--env ANTICAPTURE_API_URL="${GATEFUL_URL}" \
--meta githubCommitSha="${GIT_SHA}" \
Expand All @@ -196,7 +203,15 @@ jobs:
--meta githubOrg="${GIT_ORG}" \
--meta githubRepo="${GIT_REPO}" \
--meta githubRepoId="${GIT_REPO_ID}" \
--meta githubDeployment=1
--meta githubDeployment=1)
echo "url=${url}" >> "$GITHUB_OUTPUT"

- name: Comment preview URL on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
header: vercel-preview
message: |
🔍 **Vercel preview:** ${{ steps.deploy.outputs.url }}

changeset-path-filter:
name: Detect changeset requirement
Expand Down
53 changes: 26 additions & 27 deletions apps/api/cmd/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
HistoricalBalanceRepository,
NFTPriceRepository,
NounsVotingPowerRepository,
TORNVotingPowerRepository,
TokenRepository,
TransfersRepository,
TreasuryRepository,
Expand Down Expand Up @@ -181,7 +182,7 @@ if (!daoClient) {
}

const pgClient = drizzle(env.DATABASE_URL, {
schema,
schema: { ...schema, ...offchainSchema },
casing: "snake_case",
});

Expand Down Expand Up @@ -236,7 +237,9 @@ const votingPowerService = wrapWithTracing(
new VotingPowerService(
env.DAO_ID === DaoIdEnum.NOUNS || env.DAO_ID === DaoIdEnum.LIL_NOUNS
? wrapWithTracing(new NounsVotingPowerRepository(pgClient))
: votingPowerRepo,
: env.DAO_ID === DaoIdEnum.TORN
? wrapWithTracing(new TORNVotingPowerRepository(pgClient))
: votingPowerRepo,
votingPowerRepo,
),
);
Expand Down Expand Up @@ -396,32 +399,28 @@ if (env.DAO_ID === DaoIdEnum.ENS) {
revenue(app, revenueDuneClient);
}

if (daoClient.supportOffchainData()) {
const pgUnifiedClient = drizzle(env.DATABASE_URL, {
schema: { ...schema, ...offchainSchema },
casing: "snake_case",
});

const offchainProposalsRepo = wrapWithTracing(
new OffchainProposalRepository(pgUnifiedClient),
);
const offchainVotesRepo = wrapWithTracing(
new OffchainVoteRepository(pgUnifiedClient),
);
offchainProposals(
app,
wrapWithTracing(new OffchainProposalsService(offchainProposalsRepo)),
);
offchainVotes(
app,
wrapWithTracing(new OffchainVotesService(offchainVotesRepo)),
);
const supportOffchain = daoClient.supportOffchainData();
const offchainProposalsRepo = wrapWithTracing(
new OffchainProposalRepository(pgClient),
);
const offchainVotesRepo = wrapWithTracing(new OffchainVoteRepository(pgClient));
offchainProposals(
app,
wrapWithTracing(new OffchainProposalsService(offchainProposalsRepo)),
supportOffchain,
);
offchainVotes(
app,
wrapWithTracing(new OffchainVotesService(offchainVotesRepo)),
supportOffchain,
);

const offchainNonVotersRepo = new OffchainNonVotersRepositoryImpl(
pgUnifiedClient,
);
offchainNonVoters(app, new OffchainNonVotersService(offchainNonVotersRepo));
}
const offchainNonVotersRepo = new OffchainNonVotersRepositoryImpl(pgClient);
offchainNonVoters(
app,
wrapWithTracing(new OffchainNonVotersService(offchainNonVotersRepo)),
supportOffchain,
);

serve(
{
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/clients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./uni";
export * from "./shu";
export * from "./aave";
export * from "./fluid";
export * from "./torn";

export interface DAOClient {
getDaoId: () => string;
Expand Down
44 changes: 44 additions & 0 deletions apps/api/src/clients/torn/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const TORNGovernorAbi = [
{
type: "function",
name: "QUORUM_VOTES",
inputs: [],
outputs: [{ type: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "VOTING_DELAY",
inputs: [],
outputs: [{ type: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "VOTING_PERIOD",
inputs: [],
outputs: [{ type: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "PROPOSAL_THRESHOLD",
inputs: [],
outputs: [{ type: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "EXECUTION_DELAY",
inputs: [],
outputs: [{ type: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "EXECUTION_EXPIRATION",
inputs: [],
outputs: [{ type: "uint256" }],
stateMutability: "view",
},
] as const;
Loading
Loading