Skip to content

Commit

Permalink
add SC election to proposal monitor
Browse files Browse the repository at this point in the history
  • Loading branch information
DZGoldman committed Sep 13, 2023
1 parent 1242d03 commit 0823713
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 16 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
34 changes: 31 additions & 3 deletions src-ts/proposalMonitorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 },
Expand All @@ -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;
Expand Down Expand Up @@ -101,7 +105,7 @@ interface PipelineStage {
explorerLink?: string;
proposalLink?: string;
children: PipelineStage[];
proposalDescription?: string
proposalDescription?: string;
}

interface GovernorStatus {
Expand Down Expand Up @@ -166,7 +170,7 @@ class JsonLogger {
explorerLink: e.publicExecutionUrl,
proposalLink,
children: [],
proposalDescription: e.proposalDescription
proposalDescription: e.proposalDescription,
};

if (prevKey === originKey && !emittedStages.has(key)) {
Expand Down Expand Up @@ -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."));
2 changes: 2 additions & 0 deletions src-ts/proposalPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
RetryableExecutionStage,
UnreachableCaseError,
getProvider,
GovernorExecuteStage
} from "./proposalStage";
import { Signer } from "ethers";
import { Provider, TransactionReceipt } from "@ethersproject/abstract-provider";
Expand All @@ -26,6 +27,7 @@ export class StageFactory {

public async extractStages(receipt: TransactionReceipt): Promise<ProposalStage[]> {
return [
...(await GovernorExecuteStage.extractStages(receipt, this.arbOneSignerOrProvider)),
...(await GovernorQueueStage.extractStages(receipt, this.arbOneSignerOrProvider)),
...(await L2TimelockExecutionBatchStage.extractStages(
receipt,
Expand Down
149 changes: 137 additions & 12 deletions src-ts/proposalStage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<GovernorExecuteStage[]> {
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<ProposalStageStatus> {
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<void> {
await (
await this.governor.functions.execute(
this.targets,
this.values,
this.callDatas,
id(this.description)
)
).wait();
}

public async getExecuteReceipt(): Promise<TransactionReceipt> {
// 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<string | undefined> {
const execReceipt = await this.getExecuteReceipt();
return `https://arbiscan.io/tx/${execReceipt.transactionHash}`;
}
}

/**
* When a vote has passed, queue a proposal in the governor timelock
*/
Expand Down Expand Up @@ -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;

Expand All @@ -185,20 +310,20 @@ export class GovernorQueueStage implements ProposalStage {
public async status(): Promise<ProposalStageStatus> {
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);
Expand Down Expand Up @@ -457,7 +582,7 @@ export class L2TimelockExecutionBatchStage implements ProposalStage {

public async execute(): Promise<void> {
const timelock = ArbitrumTimelock__factory.connect(this.timelockAddress, this.signerOrProvider);

// this
const tx = await timelock.functions.executeBatch(
this.targets,
this.values,
Expand Down
44 changes: 44 additions & 0 deletions src-ts/securityCouncilElectionCreator.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

0 comments on commit 0823713

Please sign in to comment.