-
Notifications
You must be signed in to change notification settings - Fork 6
feat: integrate Tornado Cash DAO (TORN) #1783
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
base: dev
Are you sure you want to change the base?
Changes from all commits
8a48a0e
a498358
4b4f1d7
7715562
7d61998
c112c7e
b7b9845
21447d3
6af0343
6de0768
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,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,196 @@ | ||
| 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", | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| async getVotingDelay(): Promise<bigint> { | ||
| if (!this.cache.votingDelay) { | ||
| this.cache.votingDelay = (await this.readContract({ | ||
| abi: this.abi, | ||
| address: this.address, | ||
| functionName: "VOTING_DELAY", | ||
| })) as bigint; | ||
| } | ||
| return this.cache.votingDelay!; | ||
| } | ||
|
|
||
| async getVotingPeriod(): Promise<bigint> { | ||
| if (!this.cache.votingPeriod) { | ||
| this.cache.votingPeriod = (await this.readContract({ | ||
| abi: this.abi, | ||
| address: this.address, | ||
| functionName: "VOTING_PERIOD", | ||
| })) as bigint; | ||
| } | ||
| 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; | ||
| } | ||
|
|
||
| if (now < endTimestamp + executionDelay + executionExpiration) { | ||
| return ProposalStatus.PENDING_EXECUTION; | ||
| } | ||
|
|
||
| return ProposalStatus.EXPIRED; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ export const CoingeckoTokenIdEnum: Record<DaoIdEnum, string> = { | |
| ZK: "zksync", | ||
| SHU: "shutter", | ||
| FLUID: "fluid", | ||
| TORN: "tornado-cash", | ||
|
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.
Adding Useful? React with 👍 / 👎. |
||
| } as const; | ||
|
|
||
| export const CoingeckoIdToAssetPlatformId = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import type { DaoIconProps } from "@/shared/components/icons/types"; | ||
|
|
||
| export const TornadoCashIcon = ({ | ||
| showBackground = true, | ||
| ...props | ||
| }: DaoIconProps) => { | ||
| return ( | ||
| <svg | ||
| width="100%" | ||
| height="100%" | ||
| viewBox="0 0 40 40" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| {...props} | ||
| > | ||
| {showBackground && <rect width="40" height="40" fill="#1a1a2e" />} | ||
| <g transform="translate(6, 4)"> | ||
| <path | ||
| d="M14 0C6.268 0 0 6.268 0 14c0 3.866 1.568 7.37 4.104 9.906l1.414-1.414A11.937 11.937 0 0 1 2 14C2 7.373 7.373 2 14 2c2.757 0 5.302.932 7.328 2.5l1.26-1.526A13.934 13.934 0 0 0 14 0z" | ||
| fill="#94FEBF" | ||
| /> | ||
| <path | ||
| d="M14 4C8.477 4 4 8.477 4 14c0 2.762 1.12 5.262 2.932 7.068l1.414-1.414A7.965 7.965 0 0 1 6 14c0-4.418 3.582-8 8-8 1.88 0 3.61.65 4.974 1.736l1.26-1.526A9.955 9.955 0 0 0 14 4z" | ||
| fill="#94FEBF" | ||
| /> | ||
| <path | ||
| d="M14 8c-3.314 0-6 2.686-6 6 0 1.657.672 3.157 1.757 4.243l1.414-1.414A3.982 3.982 0 0 1 10 14c0-2.21 1.79-4 4-4 .964 0 1.85.34 2.54.908l1.26-1.526A5.975 5.975 0 0 0 14 8z" | ||
| fill="#94FEBF" | ||
| /> | ||
| <path d="M14 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4z" fill="#94FEBF" /> | ||
| <path | ||
| d="M22.5 5.5C24.5 9 26 12 26 16c0 3-1 5.5-3 7.5s-5 3.5-9 3.5c-2 0-3.5-.5-5-1.5 2 1.5 4.5 2 7 1.5 3-.5 5.5-2.5 7-5s2-5.5 1.5-8.5c-.3-2.5-1-5-2-8z" | ||
| fill="#94FEBF" | ||
| opacity="0.7" | ||
| /> | ||
| </g> | ||
| </svg> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TORNClient.getVotingPeriod()/getVotingDelay()currently return second-based governor constants directly, butProposalsActivityServicetreats these values as blocks and multiplies them byblockTime(apps/api/src/services/proposals-activity/index.ts, lines 123-127). For TORN this inflates the activity window by ~12x on mainnet, so the proposals-activity endpoint includes stale proposals and computes incorrect vote-timing analytics. Convert these values to block units (or add a seconds-aware path) before exposing them through the DAO client contract.Useful? React with 👍 / 👎.