From 082371387989fb1085c124f9379cecfa4e962f8f Mon Sep 17 00:00:00 2001 From: dzgoldman Date: Wed, 13 Sep 2023 16:19:05 -0400 Subject: [PATCH] add SC election to proposal monitor --- package.json | 2 +- src-ts/proposalMonitorCli.ts | 34 +++++- src-ts/proposalPipeline.ts | 2 + src-ts/proposalStage.ts | 149 +++++++++++++++++++++-- src-ts/securityCouncilElectionCreator.ts | 44 +++++++ 5 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 src-ts/securityCouncilElectionCreator.ts diff --git a/package.json b/package.json index 146ee40e..040832e4 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "coverage:report": "yarn coverage:filtered-report && yarn coverage:htmlreport && yarn coverage:open-report", "coverage:refresh": "yarn coverage:filtered-report && yarn coverage:htmlreport", "propmon:ui": "cd src-ts && http-server . -p 8080", - "propmon:service": "ts-node ./src-ts/proposalMonitorCli.ts --jsonOutputLocation ./src-ts/propMonUi/proposalState.json --l1RpcUrl $ETH_RPC --govChainRpcUrl https://arb1.arbitrum.io/rpc --novaRpcUrl https://nova.arbitrum.io/rpc --coreGovernorAddress 0xf07DeD9dC292157749B6Fd268E37DF6EA38395B9 --treasuryGovernorAddress 0x789fC99093B09aD01C34DC7251D0C89ce743e5a4 --sevenTwelveCouncil 0x895c9fc6bcf06e553b54A9fE11D948D67a9B76FA --pollingIntervalSeconds 1", + "propmon:service": "ts-node ./src-ts/proposalMonitorCli.ts --jsonOutputLocation ./src-ts/propMonUi/proposalState.json --l1RpcUrl $ETH_RPC --govChainRpcUrl https://arb1.arbitrum.io/rpc --novaRpcUrl https://nova.arbitrum.io/rpc --coreGovernorAddress 0xf07DeD9dC292157749B6Fd268E37DF6EA38395B9 --treasuryGovernorAddress 0x789fC99093B09aD01C34DC7251D0C89ce743e5a4 --sevenTwelveCouncil 0x895c9fc6bcf06e553b54A9fE11D948D67a9B76FA --nomineeElectionGovernorAddress 0x8a1cDA8dee421cD06023470608605934c16A05a0 --pollingIntervalSeconds 1", "propmon": "yarn propmon:service & yarn propmon:ui" }, "devDependencies": { diff --git a/src-ts/proposalMonitorCli.ts b/src-ts/proposalMonitorCli.ts index c97159f9..6b48909b 100644 --- a/src-ts/proposalMonitorCli.ts +++ b/src-ts/proposalMonitorCli.ts @@ -2,6 +2,8 @@ import { BigNumber, Wallet } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; import { ProposalStageStatus, getProvider } from "./proposalStage"; import { StageFactory, TrackerEventName } from "./proposalPipeline"; +import { SecurityCouncilElectionCreator } from "./securityCouncilElectionCreator"; + import * as dotenv from "dotenv"; import * as fs from "fs"; dotenv.config(); @@ -24,6 +26,7 @@ const options = yargs(process.argv.slice(2)) novaRpcUrl: { type: "string", demandOption: true }, coreGovernorAddress: { type: "string", demandOption: false }, treasuryGovernorAddress: { type: "string", demandOption: false }, + nomineeElectionGovernorAddress: { type: "string", demandOption: false }, sevenTwelveCouncilAddress: { type: "string", demandOption: false }, startBlock: { type: "number", demandOption: false, default: 72559827 }, pollingIntervalSeconds: { type: "number", demandOption: false, default: 300 }, @@ -39,6 +42,7 @@ const options = yargs(process.argv.slice(2)) coreGovernorAddress?: string; treasuryGovernorAddress?: string; sevenTwelveCouncilAddress?: string; + nomineeElectionGovernorAddress?: string; startBlock: number; pollingIntervalSeconds: number; blockLag: number; @@ -101,7 +105,7 @@ interface PipelineStage { explorerLink?: string; proposalLink?: string; children: PipelineStage[]; - proposalDescription?: string + proposalDescription?: string; } interface GovernorStatus { @@ -166,7 +170,7 @@ class JsonLogger { explorerLink: e.publicExecutionUrl, proposalLink, children: [], - proposalDescription: e.proposalDescription + proposalDescription: e.proposalDescription, }; if (prevKey === originKey && !emittedStages.has(key)) { @@ -274,7 +278,31 @@ const main = async () => { sevTwelve = startMonitor("7-12 Council", sevenTwelveMonitor, jsonLogger, options.proposalId); } - await Promise.all([roundTrip, treasury, sevTwelve]); + let electionGov; + if (options.nomineeElectionGovernorAddress) { + console.log("Starting Security Council Elections Governor Monitor"); + const electionMonitor = new GovernorProposalMonitor( + options.nomineeElectionGovernorAddress, + getProvider(govChainSignerOrProvider)!, + options.pollingIntervalSeconds * 1000, + options.blockLag, + options.startBlock, + stageFactory, + options.writeMode + ); + electionGov = startMonitor("Election", electionMonitor, jsonLogger, options.proposalId); + + if (options.writeMode) { + const electionCreator = new SecurityCouncilElectionCreator( + govChainSignerOrProvider as Wallet, + options.nomineeElectionGovernorAddress, + "0x7eCfBaa8742fDf5756DAC92fbc8b90a19b8815bF" + ); + electionCreator.run(); + } + } + + await Promise.all([roundTrip, treasury, sevTwelve, electionGov]); }; main().then(() => console.log("Done.")); diff --git a/src-ts/proposalPipeline.ts b/src-ts/proposalPipeline.ts index 53f7a3f1..6626a087 100644 --- a/src-ts/proposalPipeline.ts +++ b/src-ts/proposalPipeline.ts @@ -10,6 +10,7 @@ import { RetryableExecutionStage, UnreachableCaseError, getProvider, + GovernorExecuteStage } from "./proposalStage"; import { Signer } from "ethers"; import { Provider, TransactionReceipt } from "@ethersproject/abstract-provider"; @@ -26,6 +27,7 @@ export class StageFactory { public async extractStages(receipt: TransactionReceipt): Promise { return [ + ...(await GovernorExecuteStage.extractStages(receipt, this.arbOneSignerOrProvider)), ...(await GovernorQueueStage.extractStages(receipt, this.arbOneSignerOrProvider)), ...(await L2TimelockExecutionBatchStage.extractStages( receipt, diff --git a/src-ts/proposalStage.ts b/src-ts/proposalStage.ts index d6302728..ea9f42e6 100644 --- a/src-ts/proposalStage.ts +++ b/src-ts/proposalStage.ts @@ -19,6 +19,7 @@ import { ArbitrumTimelock__factory, L1ArbitrumTimelock__factory, L2ArbitrumGovernor__factory, + GovernorUpgradeable__factory, } from "../typechain-types"; import { Inbox__factory } from "@arbitrum/sdk/dist/lib/abi/factories/Inbox__factory"; import { EventArgs } from "@arbitrum/sdk/dist/lib/dataEntities/event"; @@ -96,9 +97,9 @@ export class UnreachableCaseError extends Error { } /** - * Taken from the GovernorTimelockUpgradeable solidity + * Taken from the IGovernorUpgradeable solidity */ -enum GovernorTimelockStatus { +enum ProposalState { Pending = 0, Active = 1, Canceled = 2, @@ -131,6 +132,122 @@ export enum ProposalStageStatus { TERMINATED = 4, } +export class GovernorExecuteStage implements ProposalStage { + public readonly name = "GovernorExecuteStage"; + public readonly identifier: string; + + public constructor( + public readonly targets: string[], + public readonly values: BigNumber[], + public readonly callDatas: string[], + public readonly description: string, + + public readonly governorAddress: string, + public readonly signerOrProvider: Signer | providers.Provider + ) { + this.identifier = keccak256( + defaultAbiCoder.encode( + ["address[]", "uint256[]", "bytes[]", "bytes32"], + [targets, values, callDatas, id(description)] + ) + ); + } + + public get governor() { + return GovernorUpgradeable__factory.connect(this.governorAddress, this.signerOrProvider); + } + + public static async extractStages( + receipt: TransactionReceipt, + arbOneSignerOrProvider: Provider | Signer + ): Promise { + const govInterface = L2ArbitrumGovernor__factory.createInterface(); + const proposalStages: GovernorExecuteStage[] = []; + // check if gov has timelock + for (const log of receipt.logs) { + if (log.topics.find((t) => t === govInterface.getEventTopic("ProposalCreated"))) { + // ensure gov has no timelock + const gov = L2ArbitrumGovernor__factory.connect(log.address, arbOneSignerOrProvider); + try { + await gov.timelock(); + continue; + } catch {} + + const propCreatedEvent = govInterface.parseLog(log) + .args as unknown as ProposalCreatedEventObject; + + proposalStages.push( + new GovernorExecuteStage( + propCreatedEvent.targets, + (propCreatedEvent as any)[3], // ethers is parsing an array with a single 0 big number as undefined, so we lookup by index + propCreatedEvent.calldatas, + propCreatedEvent.description, + log.address, + arbOneSignerOrProvider + ) + ); + } + } + + return proposalStages; + } + + public async status(): Promise { + const state = (await this.governor.state(this.identifier)) as ProposalState; + + switch (state) { + case ProposalState.Pending: + case ProposalState.Active: + return ProposalStageStatus.PENDING; + case ProposalState.Succeeded: + return ProposalStageStatus.READY; + case ProposalState.Queued: + case ProposalState.Executed: + return ProposalStageStatus.EXECUTED; + case ProposalState.Canceled: + case ProposalState.Defeated: + case ProposalState.Expired: + return ProposalStageStatus.TERMINATED; + default: + throw new UnreachableCaseError(state); + } + } + + public async execute(): Promise { + await ( + await this.governor.functions.execute( + this.targets, + this.values, + this.callDatas, + id(this.description) + ) + ).wait(); + } + + public async getExecuteReceipt(): Promise { + // TODO who does the ProposalExecuted filter not accept a string? + // @ts-ignore + const proposalExecutedFilter = this.governor.filters.ProposalExecuted(this.identifier); + const provider = getProvider(this.signerOrProvider); + + const logs = await provider!.getLogs({ + fromBlock: 0, + toBlock: "latest", + ...proposalExecutedFilter, + }); + if (logs.length !== 1) { + throw new ProposalStageError("Log length not 1", this.identifier, this.name); + } + + return await provider!.getTransactionReceipt(logs[0].transactionHash); + } + + public async getExecutionUrl(): Promise { + const execReceipt = await this.getExecuteReceipt(); + return `https://arbiscan.io/tx/${execReceipt.transactionHash}`; + } +} + /** * When a vote has passed, queue a proposal in the governor timelock */ @@ -163,6 +280,14 @@ export class GovernorQueueStage implements ProposalStage { const proposalStages: GovernorQueueStage[] = []; for (const log of receipt.logs) { if (log.topics.find((t) => t === govInterface.getEventTopic("ProposalCreated"))) { + // ensure gov has timelock + const gov = L2ArbitrumGovernor__factory.connect(log.address, arbOneSignerOrProvider); + try { + await gov.timelock(); + } catch { + continue; + } + const propCreatedEvent = govInterface.parseLog(log) .args as unknown as ProposalCreatedEventObject; @@ -185,20 +310,20 @@ export class GovernorQueueStage implements ProposalStage { public async status(): Promise { const gov = L2ArbitrumGovernor__factory.connect(this.governorAddress, this.signerOrProvider); - const state = (await gov.state(this.identifier)) as GovernorTimelockStatus; + const state = (await gov.state(this.identifier)) as ProposalState; switch (state) { - case GovernorTimelockStatus.Pending: - case GovernorTimelockStatus.Active: + case ProposalState.Pending: + case ProposalState.Active: return ProposalStageStatus.PENDING; - case GovernorTimelockStatus.Succeeded: + case ProposalState.Succeeded: return ProposalStageStatus.READY; - case GovernorTimelockStatus.Queued: - case GovernorTimelockStatus.Executed: + case ProposalState.Queued: + case ProposalState.Executed: return ProposalStageStatus.EXECUTED; - case GovernorTimelockStatus.Canceled: - case GovernorTimelockStatus.Defeated: - case GovernorTimelockStatus.Expired: + case ProposalState.Canceled: + case ProposalState.Defeated: + case ProposalState.Expired: return ProposalStageStatus.TERMINATED; default: throw new UnreachableCaseError(state); @@ -457,7 +582,7 @@ export class L2TimelockExecutionBatchStage implements ProposalStage { public async execute(): Promise { const timelock = ArbitrumTimelock__factory.connect(this.timelockAddress, this.signerOrProvider); - + // this const tx = await timelock.functions.executeBatch( this.targets, this.values, diff --git a/src-ts/securityCouncilElectionCreator.ts b/src-ts/securityCouncilElectionCreator.ts new file mode 100644 index 00000000..fd86e6ce --- /dev/null +++ b/src-ts/securityCouncilElectionCreator.ts @@ -0,0 +1,44 @@ +import { Wallet } from "ethers"; +import { Multicall2__factory } from "../token-bridge-contracts/build/types"; +import { SecurityCouncilNomineeElectionGovernor__factory } from "../typechain-types"; + +export class SecurityCouncilElectionCreator { + retryTime = 10 * 1000; + public constructor( + public readonly connectedSigner: Wallet, + public readonly nomineeElectionGovAddress: string, + public readonly multicallAddress: string + ) {} + + public async checkAndCreateElection() { + const multicall = Multicall2__factory.connect( + this.multicallAddress, + this.connectedSigner.provider + ); + const gov = SecurityCouncilNomineeElectionGovernor__factory.connect( + this.nomineeElectionGovAddress, + this.connectedSigner.provider + ); + const parentChainTimestamp = await multicall.getCurrentBlockTimestamp(); + const electionTimestamp = await gov.electionToTimestamp(await gov.electionCount()); + + const timeToElectionSeconds = electionTimestamp.sub(parentChainTimestamp).toNumber(); + if (timeToElectionSeconds <= 0) { + const res = await gov.createElection(); + await res.wait(); + setTimeout(this.run, this.retryTime); + } else { + console.log(`Next election starts in ${timeToElectionSeconds} seconds`); + setTimeout(this.run, Math.max(timeToElectionSeconds * 1000, this.retryTime)); + } + } + + public async run() { + try { + this.checkAndCreateElection(); + } catch (e) { + console.log("SecurityCouncilElectionCreator error:", e); + setTimeout(this.run, this.retryTime); + } + } +}