Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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;
196 changes: 196 additions & 0 deletions apps/api/src/clients/torn/index.ts
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;
Comment on lines +56 to +60
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 Return Torn voting windows in block units

TORNClient.getVotingPeriod()/getVotingDelay() currently return second-based governor constants directly, but ProposalsActivityService treats these values as blocks and multiplies them by blockTime (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 👍 / 👎.

}
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;
}
}
5 changes: 5 additions & 0 deletions apps/api/src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SHUClient,
AAVEClient,
FLUIDClient,
TORNClient,
} from "@/clients";

import { CONTRACT_ADDRESSES } from "./constants";
Expand Down Expand Up @@ -84,6 +85,10 @@ export function getClient<
case DaoIdEnum.AAVE: {
return new AAVEClient(client);
}
case DaoIdEnum.TORN: {
const { governor } = CONTRACT_ADDRESSES[daoId];
return new TORNClient(client, governor.address);
}
default:
return null;
}
Expand Down
14 changes: 14 additions & 0 deletions apps/api/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,19 @@ export const CONTRACT_ADDRESSES = {
startBlock: 12422079,
},
},
[DaoIdEnum.TORN]: {
blockTime: 12,
tokenType: "ERC20",
token: {
address: "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C",
decimals: 18,
startBlock: 11474599,
},
governor: {
address: "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce",
startBlock: 11474695,
},
},
} as const;

export const TreasuryAddresses: Record<DaoIdEnum, Record<string, Address>> = {
Expand Down Expand Up @@ -390,6 +403,7 @@ export const TreasuryAddresses: Record<DaoIdEnum, Record<string, Address>> = {
"0x639f35C5E212D61Fe14Bd5CD8b66aAe4df11a50c",
InstaTimelock: "0xC7Cb1dE2721BFC0E0DA1b9D526bCdC54eF1C0eFC",
},
[DaoIdEnum.TORN]: {},
};

export enum ProposalStatus {
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/lib/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum DaoIdEnum {
OBOL = "OBOL",
ZK = "ZK",
FLUID = "FLUID",
TORN = "TORN",
}

export const SECONDS_IN_DAY = 24 * 60 * 60;
Expand Down
7 changes: 7 additions & 0 deletions apps/api/src/lib/eventRelevance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,13 @@ const DAO_RELEVANCE_THRESHOLDS: Record<DaoIdEnum, EventRelevanceMap> = {
[FeedEventType.PROPOSAL]: EMPTY_THRESHOLDS,
[FeedEventType.PROPOSAL_EXTENDED]: EMPTY_THRESHOLDS,
},
[DaoIdEnum.TORN]: {
[FeedEventType.TRANSFER]: EMPTY_THRESHOLDS,
[FeedEventType.DELEGATION]: EMPTY_THRESHOLDS,
[FeedEventType.VOTE]: EMPTY_THRESHOLDS,
[FeedEventType.PROPOSAL]: EMPTY_THRESHOLDS,
[FeedEventType.PROPOSAL_EXTENDED]: EMPTY_THRESHOLDS,
},
};

export function getDaoRelevanceThreshold(daoId: DaoIdEnum): EventRelevanceMap {
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/services/coingecko/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const CoingeckoTokenIdEnum: Record<DaoIdEnum, string> = {
ZK: "zksync",
SHU: "shutter",
FLUID: "fluid",
TORN: "tornado-cash",
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 Map TORN token ID to a CoinGecko asset platform

Adding TORN to CoingeckoTokenIdEnum without adding a corresponding entry in CoingeckoIdToAssetPlatformId makes CoingeckoService.getTokenPrice() resolve assetPlatform as undefined for TORN, so it requests /simple/token_price/undefined?... and fails token price lookups for TORN-backed endpoints.

Useful? React with 👍 / 👎.

} as const;

export const CoingeckoIdToAssetPlatformId = {
Expand Down
39 changes: 39 additions & 0 deletions apps/dashboard/shared/components/icons/TornadoCashIcon.tsx
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>
);
};
1 change: 1 addition & 0 deletions apps/dashboard/shared/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from "@/shared/components/icons/ScrollIcon";
export * from "@/shared/components/icons/CompoundIcon";
export * from "@/shared/components/icons/ObolIcon";
export * from "@/shared/components/icons/ShutterIcon";
export * from "@/shared/components/icons/TornadoCashIcon";
// THE IMPORT OF DAO AVATAR ICON MUST BE LAST
export * from "@/shared/components/icons/DaoAvatarIcon";
export * from "@/shared/components/icons/CookieBackground";
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/shared/dao-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { OBOL } from "@/shared/dao-config/obol";
import { OP } from "@/shared/dao-config/op";
import { SCR } from "@/shared/dao-config/scr";
import { SHU } from "@/shared/dao-config/shu";
import { TORN } from "@/shared/dao-config/torn";
import { UNI } from "@/shared/dao-config/uni";
import { AAVE } from "@/shared/dao-config/aave";

Expand All @@ -24,4 +25,5 @@ export default {
COMP,
OBOL,
SHU,
TORN,
} as const;
Loading
Loading