Skip to content

Commit

Permalink
feat: add timelock-id in notifications (#13)
Browse files Browse the repository at this point in the history
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
<img width="543" alt="Screenshot 2024-08-05 at 14 48 03"
src="https://github.com/user-attachments/assets/0b625587-b62a-48ec-94f4-51e35be71f7d">

Telegram

![image](https://github.com/user-attachments/assets/97fae6e7-b35e-4e84-96bd-a12c3b006a1b)

---------

Co-authored-by: Philip Paetz <philip.paetz@me.com>
  • Loading branch information
nvtaveras and chapati23 authored Aug 6, 2024
1 parent 109c586 commit 1a5e7d4
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 10 deletions.
26 changes: 19 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions src/parse-transaction-receipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +22,7 @@ export default function parseTransactionReceipts(
): {
block?: number;
event: ProposalCreatedEvent | HealthCheckEvent;
timelockId?: string;
txHash: string;
}[] {
const result = [];
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -95,6 +100,7 @@ export default function parseTransactionReceipts(
});
break;
}

default:
assert(
false,
Expand Down
5 changes: 5 additions & 0 deletions src/send-discord-notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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({
Expand Down
2 changes: 2 additions & 0 deletions src/send-telegram-notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
};

Expand Down
1 change: 0 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export interface ProposalCreatedEvent {

export interface HealthCheckEvent {
eventName: EventType.MedianUpdated;
block: number;
args: {
token: `0x${string}`;
value: bigint;
Expand Down
27 changes: 27 additions & 0 deletions src/utils/get-time-lock-id.ts
Original file line number Diff line number Diff line change
@@ -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],
),
);
}

0 comments on commit 1a5e7d4

Please sign in to comment.