From 4afadb80be755754387ef02959d3cb6c6fe34d5d Mon Sep 17 00:00:00 2001
From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com>
Date: Tue, 6 Aug 2024 11:01:52 +0200
Subject: [PATCH] feat: add timelock-id in notifications (#13)
This adds the timelock ID to the discord/telegram notification messages
so that it can be used by members of the watchdog group to veto a
proposal if needed.
Discord
Telegram
![image](https://github.com/user-attachments/assets/97fae6e7-b35e-4e84-96bd-a12c3b006a1b)
---------
Co-authored-by: Philip Paetz
---
src/index.ts | 26 +++++++++++++++++++-------
src/parse-transaction-receipts.ts | 10 ++++++++--
src/send-discord-notification.ts | 5 +++++
src/send-telegram-notification.ts | 2 ++
src/types.ts | 1 -
src/utils/get-time-lock-id.ts | 27 +++++++++++++++++++++++++++
6 files changed, 61 insertions(+), 10 deletions(-)
create mode 100644 src/utils/get-time-lock-id.ts
diff --git a/src/index.ts b/src/index.ts
index 4074d1b..3c7b19b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,16 +1,14 @@
-import assert from "assert/strict";
-
-// Types
import type {
HttpFunction,
Request,
Response,
} from "@google-cloud/functions-framework";
-import { EventType } from "./types.js";
+import assert from "assert/strict";
import parseTransactionReceipts from "./parse-transaction-receipts";
import sendDiscordNotification from "./send-discord-notification";
import sendTelegramNotification from "./send-telegram-notification";
-import { isFromQuicknode, hasAuthToken } from "./validate-request-origin";
+import { EventType } from "./types.js";
+import { hasAuthToken, isFromQuicknode } from "./validate-request-origin";
export const watchdogNotifier: HttpFunction = async (
req: Request,
@@ -34,13 +32,27 @@ export const watchdogNotifier: HttpFunction = async (
for (const parsedEvent of parsedEvents) {
switch (parsedEvent.event.eventName) {
case EventType.ProposalCreated:
- await sendDiscordNotification(parsedEvent.event, parsedEvent.txHash);
- await sendTelegramNotification(parsedEvent.event, parsedEvent.txHash);
+ assert(parsedEvent.timelockId, "Timelock ID is missing");
+
+ await sendDiscordNotification(
+ parsedEvent.event,
+ parsedEvent.timelockId,
+ parsedEvent.txHash,
+ );
+
+ await sendTelegramNotification(
+ parsedEvent.event,
+ parsedEvent.timelockId,
+ parsedEvent.txHash,
+ );
+
break;
+
case EventType.MedianUpdated:
// Acts a health check/heartbeat for the service, as it's a frequently emitted event
console.info("[HealthCheck]: Block", parsedEvent.block);
break;
+
default:
assert(
false,
diff --git a/src/parse-transaction-receipts.ts b/src/parse-transaction-receipts.ts
index d869a2b..24a2e3e 100644
--- a/src/parse-transaction-receipts.ts
+++ b/src/parse-transaction-receipts.ts
@@ -5,13 +5,14 @@ import { decodeEventLog } from "viem";
// Internal
import GovernorABI from "./governor-abi.js";
+import SortedOraclesABI from "./sorted-oracles-abi.js";
import { EventType, HealthCheckEvent, ProposalCreatedEvent } from "./types.js";
-import hasLogs from "./utils/has-logs.js";
import getEventByTopic from "./utils/get-event-by-topic.js";
+import getProposaltimelockId from "./utils/get-time-lock-id.js";
+import hasLogs from "./utils/has-logs.js";
import isHealthCheckEvent from "./utils/is-health-check-event.js";
import isProposalCreatedEvent from "./utils/is-proposal-created-event.js";
import isTransactionReceipt from "./utils/is-transaction-receipt.js";
-import SortedOraclesABI from "./sorted-oracles-abi.js";
/**
* Parse request body containing raw transaction receipts
@@ -21,6 +22,7 @@ export default function parseTransactionReceipts(
): {
block?: number;
event: ProposalCreatedEvent | HealthCheckEvent;
+ timelockId?: string;
txHash: string;
}[] {
const result = [];
@@ -58,6 +60,7 @@ export default function parseTransactionReceipts(
// It can happen that a single transaction fires multiple events,
// some of which we are not interested in
continue;
+
case EventType.ProposalCreated: {
const event = decodeEventLog({
abi: GovernorABI,
@@ -72,10 +75,12 @@ export default function parseTransactionReceipts(
result.push({
event,
+ timelockId: getProposaltimelockId(event),
txHash: log.transactionHash,
});
break;
}
+
case EventType.MedianUpdated: {
const event = decodeEventLog({
abi: SortedOraclesABI,
@@ -95,6 +100,7 @@ export default function parseTransactionReceipts(
});
break;
}
+
default:
assert(
false,
diff --git a/src/send-discord-notification.ts b/src/send-discord-notification.ts
index ee7a234..5336483 100644
--- a/src/send-discord-notification.ts
+++ b/src/send-discord-notification.ts
@@ -5,6 +5,7 @@ import type { ProposalCreatedEvent } from "./types";
export default async function sendDiscordNotification(
event: ProposalCreatedEvent,
+ timelockId: string,
txHash: string,
) {
const { title, description } = JSON.parse(event.args.description) as {
@@ -28,6 +29,10 @@ export default async function sendDiscordNotification(
name: "Transaction",
value: `https://celoscan.io/tx/${txHash}`,
})
+ .addFields({
+ name: "Timelock ID",
+ value: timelockId,
+ })
.setColor(0xa6e5f6);
const discordWebhookClient = new WebhookClient({
diff --git a/src/send-telegram-notification.ts b/src/send-telegram-notification.ts
index bd38043..f54a9b1 100644
--- a/src/send-telegram-notification.ts
+++ b/src/send-telegram-notification.ts
@@ -4,6 +4,7 @@ import { ProposalCreatedEvent } from "./types";
export default async function sendTelegramNotification(
event: ProposalCreatedEvent,
+ timelockId: string,
txHash: string,
) {
const botToken = await getSecret(config.TELEGRAM_BOT_TOKEN_SECRET_ID);
@@ -19,6 +20,7 @@ export default async function sendTelegramNotification(
Proposer: `https://celoscan.io/address/${event.args.proposer}`,
Event: event.eventName,
Transaction: `https://celoscan.io/tx/${txHash}`,
+ "Timelock ID": timelockId,
Description: description,
};
diff --git a/src/types.ts b/src/types.ts
index 47390d4..0c5934e 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -50,7 +50,6 @@ export interface ProposalCreatedEvent {
export interface HealthCheckEvent {
eventName: EventType.MedianUpdated;
- block: number;
args: {
token: `0x${string}`;
value: bigint;
diff --git a/src/utils/get-time-lock-id.ts b/src/utils/get-time-lock-id.ts
new file mode 100644
index 0000000..67e730f
--- /dev/null
+++ b/src/utils/get-time-lock-id.ts
@@ -0,0 +1,27 @@
+import { encodeAbiParameters, keccak256, parseAbiParameters } from "viem";
+
+import { ProposalCreatedEvent } from "../types";
+
+/**
+ * Given a ProposalCreatedEvent, calculate the corresponding timelock operation ID.
+ * Governance Watchdogs need the timelock operation ID to veto queued proposals.
+ *
+ * The governor proposal ID and the timelock operation ID are not the same, which can
+ * be confusing. They use different hashing mechanisms to calculate their respective IDs:
+ * - Timelock Controller Operation IDs: https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/58fa0f81c4036f1a3b616fdffad2fd27e5d5ce21/contracts/governance/TimelockControllerUpgradeable.sol#L218
+ * - Governor Proposal IDs: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/0a25c1940ca220686588c4af3ec526f725fe2582/contracts/governance/Governor.sol#L139
+ */
+export default function getProposalTimeLockId(
+ event: ProposalCreatedEvent,
+): string {
+ const { targets, values, calldatas, description } = event.args;
+ const descriptionHash = keccak256(Buffer.from(description));
+
+ return keccak256(
+ encodeAbiParameters(
+ parseAbiParameters("address[], uint256[], bytes[], uint256, bytes32"),
+ // _timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, descriptionHash);
+ [targets, values, calldatas, 0n, descriptionHash],
+ ),
+ );
+}