diff --git a/.changeset/add-shutter-security-council.md b/.changeset/add-shutter-security-council.md new file mode 100644 index 000000000..126b4c92d --- /dev/null +++ b/.changeset/add-shutter-security-council.md @@ -0,0 +1,5 @@ +--- +"@anticapture/dashboard": minor +--- + +Add the Security Council card to the Shutter DAO overview: the 5-of-8 multisig that owns the Snapshot X space and vetoes proposals via `cancel` before execution. Support councils with no fixed term ("No expiration") in the card. diff --git a/apps/dashboard/features/dao-overview/components/CountdownDaoInfo.tsx b/apps/dashboard/features/dao-overview/components/CountdownDaoInfo.tsx index f2bc177d5..0d29e26d2 100644 --- a/apps/dashboard/features/dao-overview/components/CountdownDaoInfo.tsx +++ b/apps/dashboard/features/dao-overview/components/CountdownDaoInfo.tsx @@ -15,6 +15,11 @@ export const CountdownDaoInfo = ({ const targetTimestamp = securityCouncil?.expiration.timestamp; const countdown = useCountdown(targetTimestamp); + // timestamp === 0 marks a council with no fixed term: show a static label + // instead of a countdown. + const noExpiration = + !!securityCouncil && !securityCouncil.expiration.timestamp; + const formattedCountdown = useMemo(() => { if (!countdown || countdown.isLoading) return null; return { @@ -25,6 +30,14 @@ export const CountdownDaoInfo = ({ }; }, [countdown]); + if (noExpiration) { + return ( +
+ No expiration +
+ ); + } + if (!formattedCountdown) return null; return ( diff --git a/apps/dashboard/features/dao-overview/components/SecurityCouncilCard.tsx b/apps/dashboard/features/dao-overview/components/SecurityCouncilCard.tsx index 75cf7cc86..43dcc1f42 100644 --- a/apps/dashboard/features/dao-overview/components/SecurityCouncilCard.tsx +++ b/apps/dashboard/features/dao-overview/components/SecurityCouncilCard.tsx @@ -17,7 +17,8 @@ export const SecurityCouncilCard = ({ const { securityCouncil } = daoOverview; const progress = useMemo(() => { - if (!securityCouncil) return 0; + // timestamp === 0 marks a council with no fixed term: no progress to show. + if (!securityCouncil || !securityCouncil.expiration.timestamp) return 0; const start = new Date(securityCouncil.expiration.startDate).getTime(); const end = new Date(securityCouncil.expiration.date).getTime(); const now = Date.now(); @@ -29,7 +30,8 @@ export const SecurityCouncilCard = ({ }, [securityCouncil]); const warning = useMemo(() => { - if (!securityCouncil) return 0; + // No fixed term (timestamp === 0) means no danger zone to render. + if (!securityCouncil || !securityCouncil.expiration.timestamp) return 0; const start = new Date(securityCouncil.expiration.startDate).getTime(); const end = new Date(securityCouncil.expiration.date).getTime(); diff --git a/apps/dashboard/shared/dao-config/shu.ts b/apps/dashboard/shared/dao-config/shu.ts index 2ee3d55d9..5dec4506e 100644 --- a/apps/dashboard/shared/dao-config/shu.ts +++ b/apps/dashboard/shared/dao-config/shu.ts @@ -51,6 +51,32 @@ export const SHU: DaoConfiguration = { name: "Anticapture", url: toAbsoluteUrl("/shu/proposals/"), }, + securityCouncil: { + isActive: true, + // Shutter's veto is exercised through Snapshot X: the Security Council + // (5-of-8 Safe 0x3ea7...c0C0) owns the Snapshot X Space + // (0x594EB60b35C4E91A06a5df988e0504f7463cB769) and vetoes a proposal by + // calling Space.cancel(proposalId) any time before it is executed. + vetoCouncilAddress: "0x3ea731dAF66D6A7980549f90152CD9A761B9c0C0", + multisig: { + threshold: 5, + signers: 8, + externalLink: + "https://app.safe.global/home?safe=eth:0x3ea731dAF66D6A7980549f90152CD9A761B9c0C0", + description: + "The Security Council is a multisig with eight signers, needing five signatures to veto a proposal by cancelling it on the Snapshot X space before execution.", + }, + expiration: { + // The veto is an owner privilege with no fixed term: timestamp 0 makes + // the card show "No expiration" instead of a countdown. Only startDate + // is rendered (as "Start") — the council took ownership of the Space on + // 2026-09-07; the other fields are unused. + startDate: "September 7, 2026", + date: "", + timestamp: 0, + alertExpiration: 0, + }, + }, }, attackProfitability: { riskLevel: RiskLevel.HIGH, diff --git a/apps/dashboard/shared/dao-config/types.ts b/apps/dashboard/shared/dao-config/types.ts index 38e24cdd7..3624b589d 100644 --- a/apps/dashboard/shared/dao-config/types.ts +++ b/apps/dashboard/shared/dao-config/types.ts @@ -114,6 +114,13 @@ export interface DaoOverviewConfig { }; expiration: { date: string; + /** + * Unix seconds when the veto term ends. Use `0` for a council with no + * fixed term (e.g. a Safe that owns a governance module and can cancel + * indefinitely): the card then shows "No expiration" instead of a + * countdown and hides the progress/danger bar, and the other expiration + * fields are ignored (only `startDate` is still shown as "Start"). + */ timestamp: number; startDate: string; alertExpiration: number;