-
Notifications
You must be signed in to change notification settings - Fork 7
feat: integrate tornado cash #2002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
451db65
c9f71b5
f542b75
2a69466
4e60b18
d94e2a0
cbd82c1
3086165
5cc2176
db66cd3
d8cdb61
257f918
a6694d9
3031315
f1ae0fb
00f4c26
1c07ed3
01a29aa
4ff84b4
4d96c51
2c71302
c4bc69a
36992d7
032a710
236fe26
593fc57
1735924
203ea72
cdb00d2
2f0aca6
2fc7174
0d2da65
96c0f22
412b9e8
3129964
6671b7c
27aa217
1454329
1a91eb5
258eaef
d3dbc1e
7923fb5
e2202b4
1877182
e8cd7ac
732e60b
937c42f
873bb45
bb0a8f0
6c4fd5b
2eed136
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| import { Account, Address, Chain, Client, Transport } from "viem"; | ||
|
|
||
| import { DAOClient } from "@/clients"; | ||
| import { ProposalStatus } from "@/lib/constants"; | ||
|
|
||
| import { GovernorBase } from "../governor.base"; | ||
|
|
||
| import { TORNGovernorAbi } from "./abi"; | ||
|
|
||
| const BLOCK_TIME = 12; | ||
|
|
||
| export class TORNClient< | ||
| TTransport extends Transport = Transport, | ||
| TChain extends Chain = Chain, | ||
| TAccount extends Account | undefined = Account | undefined, | ||
| > | ||
| extends GovernorBase | ||
| implements DAOClient | ||
| { | ||
| protected address: Address; | ||
| protected abi: typeof TORNGovernorAbi; | ||
|
|
||
| constructor(client: Client<TTransport, TChain, TAccount>, address: Address) { | ||
| super(client); | ||
| this.address = address; | ||
| this.abi = TORNGovernorAbi; | ||
| } | ||
|
|
||
| getDaoId(): string { | ||
| return "TORN"; | ||
| } | ||
|
|
||
| async getQuorum(_proposalId: string | null): Promise<bigint> { | ||
| return this.getCachedQuorum(async () => { | ||
| return this.readContract({ | ||
| abi: this.abi, | ||
| address: this.address, | ||
| functionName: "QUORUM_VOTES", | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| // TORN governance times everything in seconds, but the platform contract | ||
| // for getVotingDelay/getVotingPeriod expects BLOCK units (consumers multiply | ||
| // by blockTime to recover seconds). Convert seconds → blocks on read. | ||
| async getVotingDelay(): Promise<bigint> { | ||
| if (!this.cache.votingDelay) { | ||
| const seconds = (await this.readContract({ | ||
| abi: this.abi, | ||
| address: this.address, | ||
| functionName: "VOTING_DELAY", | ||
| })) as bigint; | ||
| this.cache.votingDelay = seconds / BigInt(BLOCK_TIME); | ||
| } | ||
| return this.cache.votingDelay!; | ||
| } | ||
|
|
||
| async getVotingPeriod(): Promise<bigint> { | ||
| if (!this.cache.votingPeriod) { | ||
| const seconds = (await this.readContract({ | ||
| abi: this.abi, | ||
| address: this.address, | ||
| functionName: "VOTING_PERIOD", | ||
| })) as bigint; | ||
| this.cache.votingPeriod = seconds / BigInt(BLOCK_TIME); | ||
| } | ||
| return this.cache.votingPeriod!; | ||
| } | ||
|
|
||
| async getProposalThreshold(): Promise<bigint> { | ||
| if (!this.cache.proposalThreshold) { | ||
| this.cache.proposalThreshold = (await this.readContract({ | ||
| abi: this.abi, | ||
| address: this.address, | ||
| functionName: "PROPOSAL_THRESHOLD", | ||
| })) as bigint; | ||
| } | ||
| return this.cache.proposalThreshold!; | ||
| } | ||
|
|
||
| async getTimelockDelay(): Promise<bigint> { | ||
| if (!this.cache.timelockDelay) { | ||
| this.cache.timelockDelay = (await this.readContract({ | ||
| abi: this.abi, | ||
| address: this.address, | ||
| functionName: "EXECUTION_DELAY", | ||
| })) as bigint; | ||
| } | ||
| return this.cache.timelockDelay!; | ||
| } | ||
|
|
||
| private async getExecutionExpiration(): Promise<bigint> { | ||
| if (!this.cache.executionPeriod) { | ||
| this.cache.executionPeriod = (await this.readContract({ | ||
| abi: this.abi, | ||
| address: this.address, | ||
| functionName: "EXECUTION_EXPIRATION", | ||
| })) as bigint; | ||
| } | ||
| return this.cache.executionPeriod!; | ||
| } | ||
|
|
||
| calculateQuorum(votes: { | ||
| forVotes: bigint; | ||
| againstVotes: bigint; | ||
| abstainVotes: bigint; | ||
| }): bigint { | ||
| return votes.forVotes; | ||
| } | ||
|
|
||
| alreadySupportCalldataReview(): boolean { | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Tornado Cash proposal status — timestamp-based, not block-based. | ||
| * | ||
| * The governor uses seconds for all timing parameters (VOTING_DELAY, | ||
| * VOTING_PERIOD, EXECUTION_DELAY, EXECUTION_EXPIRATION). We derive | ||
| * startTime synthetically from endTimestamp and the block range. | ||
| * | ||
| * State machine: | ||
| * EXECUTED → finalized (persisted by indexer) | ||
| * now < startTime → PENDING | ||
| * now < endTimestamp → ACTIVE | ||
| * forVotes < quorum → NO_QUORUM | ||
| * forVotes <= againstVotes → DEFEATED | ||
| * now < endTimestamp + EXECUTION_DELAY → QUEUED | ||
| * now < endTimestamp + EXECUTION_DELAY + EXECUTION_EXPIRATION → PENDING_EXECUTION | ||
| * else → EXPIRED | ||
| */ | ||
| async getProposalStatus( | ||
| proposal: { | ||
| id: string; | ||
| status: string; | ||
| startBlock: number; | ||
| endBlock: number; | ||
| forVotes: bigint; | ||
| againstVotes: bigint; | ||
| abstainVotes: bigint; | ||
| endTimestamp: bigint; | ||
| }, | ||
| _currentBlock: number, | ||
| currentTimestamp: number, | ||
| ): Promise<string> { | ||
| // Already finalized via event | ||
| if (proposal.status === ProposalStatus.EXECUTED) { | ||
| return ProposalStatus.EXECUTED; | ||
| } | ||
|
|
||
| if (proposal.status === ProposalStatus.CANCELED) { | ||
| return ProposalStatus.CANCELED; | ||
| } | ||
|
|
||
| const now = BigInt(currentTimestamp); | ||
| const endTimestamp = proposal.endTimestamp; | ||
|
|
||
| // Estimate startTime from endTimestamp and block range | ||
| const votingDurationSeconds = | ||
| BigInt(proposal.endBlock - proposal.startBlock) * BigInt(BLOCK_TIME); | ||
| const startTime = endTimestamp - votingDurationSeconds; | ||
|
|
||
| if (now < startTime) { | ||
| return ProposalStatus.PENDING; | ||
| } | ||
|
|
||
| if (now < endTimestamp) { | ||
| return ProposalStatus.ACTIVE; | ||
| } | ||
|
|
||
| // After voting period ends — check quorum and majority | ||
| const quorum = await this.getQuorum(proposal.id); | ||
| const proposalQuorum = this.calculateQuorum({ | ||
| forVotes: proposal.forVotes, | ||
| againstVotes: proposal.againstVotes, | ||
| abstainVotes: proposal.abstainVotes, | ||
| }); | ||
|
|
||
| if (proposalQuorum < quorum) { | ||
| return ProposalStatus.NO_QUORUM; | ||
| } | ||
|
|
||
| if (proposal.forVotes <= proposal.againstVotes) { | ||
| return ProposalStatus.DEFEATED; | ||
| } | ||
|
|
||
| // Passed — check timelock windows | ||
| const executionDelay = await this.getTimelockDelay(); | ||
| const executionExpiration = await this.getExecutionExpiration(); | ||
|
|
||
| if (now < endTimestamp + executionDelay) { | ||
| return ProposalStatus.QUEUED; | ||
|
pikonha marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if (now < endTimestamp + executionDelay + executionExpiration) { | ||
| return ProposalStatus.PENDING_EXECUTION; | ||
|
Comment on lines
+197
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For TORN proposals that reach this computed state, the dashboard renders its generic Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| return ProposalStatus.EXPIRED; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.