From 8da470c38f1db0e2fba6e86601de334d99405fb3 Mon Sep 17 00:00:00 2001 From: dzgoldman Date: Fri, 17 Nov 2023 12:15:32 -0500 Subject: [PATCH 1/4] extract batch timelock scheduled stage not submitted by a governor (CallScheduled event, not just ProposalQueued) --- src-ts/proposalStage.ts | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src-ts/proposalStage.ts b/src-ts/proposalStage.ts index 0915c410..a3a04c6c 100644 --- a/src-ts/proposalStage.ts +++ b/src-ts/proposalStage.ts @@ -548,6 +548,8 @@ export class L2TimelockExecutionBatchStage extends L2TimelockExecutionStage { ): Promise { const govInterface = L2ArbitrumGovernor__factory.createInterface(); const proposalStages: L2TimelockExecutionBatchStage[] = []; + const timelockInterface = ArbitrumTimelock__factory.createInterface(); + for (const log of receipt.logs) { if (log.topics.find((t) => t === govInterface.getEventTopic("ProposalQueued"))) { const propQueuedObj = govInterface.parseLog(log) @@ -594,6 +596,50 @@ export class L2TimelockExecutionBatchStage extends L2TimelockExecutionStage { arbOneSignerOrProvider ); proposalStages.push(executeBatch); + } else if (log.topics[0] === timelockInterface.getEventTopic("CallScheduled")){ + try { + const callScheduledArgs = timelockInterface.parseLog(log) + .args as CallScheduledEvent["args"]; + + const { data } = ArbSys__factory.createInterface().decodeFunctionData( + "sendTxToL1", + callScheduledArgs.data + ); + + const { salt } = this.decodeScheduleBatch(data); + + // calculate the id and check if that operation exists + const operationId = this.hashOperationBatch( + [callScheduledArgs.target], + [callScheduledArgs[3]], // cant use .value as ethers fails with this + [callScheduledArgs.data], + callScheduledArgs.predecessor, + salt + ); + + if (operationId !== callScheduledArgs.id) { + throw new Error("Invalid operation id"); + } + + const timelockAddress = log.address; + const executeTimelock = new L2TimelockExecutionBatchStage( + [callScheduledArgs.target], + [callScheduledArgs[3]], + [callScheduledArgs.data], + callScheduledArgs.predecessor, + salt, + timelockAddress, + arbOneSignerOrProvider + ); + if ( + proposalStages.filter((s) => s.identifier === executeTimelock.identifier).length === 0 + ) { + proposalStages.push(executeTimelock); + } + } catch (err) { + // there are expected errors since the calldata may not be of the expected form for decoding + continue; + } } } From 289123e6a4a15cd46fca8190cab9d6b6cc1d7123 Mon Sep 17 00:00:00 2001 From: dzgoldman Date: Fri, 17 Nov 2023 12:16:33 -0500 Subject: [PATCH 2/4] update 7 of 12 address to latest and fix cl arg in script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa8a840a..5279f8ea 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 --nomineeElectionGovernorAddress 0x8a1cDA8dee421cD06023470608605934c16A05a0 --pollingIntervalSeconds 5", + "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 --sevenTwelveCouncilAddress 0xADd68bCb0f66878aB9D37a447C7b9067C5dfa941 --nomineeElectionGovernorAddress 0x8a1cDA8dee421cD06023470608605934c16A05a0 --pollingIntervalSeconds 5", "propmon": "yarn propmon:service & yarn propmon:ui" }, "devDependencies": { From ba3b8ab9efff90469c4fe529239e62b2e9402b16 Mon Sep 17 00:00:00 2001 From: dzgoldman Date: Fri, 17 Nov 2023 12:16:53 -0500 Subject: [PATCH 3/4] add Non-emergency Security Council to propmon html --- src-ts/propMonUi/propMonUi.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src-ts/propMonUi/propMonUi.html b/src-ts/propMonUi/propMonUi.html index 16f09c51..8ce26183 100644 --- a/src-ts/propMonUi/propMonUi.html +++ b/src-ts/propMonUi/propMonUi.html @@ -175,6 +175,8 @@

Proposal States

return 'Treasury Governor'; case '0x8a1cDA8dee421cD06023470608605934c16A05a0'.toLowerCase(): return 'Election Governor' + case '0xADd68bCb0f66878aB9D37a447C7b9067C5dfa941'.toLowerCase(): + return 'Non-emergency Security Council' default: return 'Unknown Governor';; } From 5ac1a36394221ccd84270dc73f16156b81222290 Mon Sep 17 00:00:00 2001 From: dzgoldman Date: Mon, 20 Nov 2023 13:45:54 -0500 Subject: [PATCH 4/4] handle batches L2TimelockExecutionBatchStage extract stages --- src-ts/proposalStage.ts | 78 +++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/src-ts/proposalStage.ts b/src-ts/proposalStage.ts index a3a04c6c..c1b229b0 100644 --- a/src-ts/proposalStage.ts +++ b/src-ts/proposalStage.ts @@ -549,6 +549,8 @@ export class L2TimelockExecutionBatchStage extends L2TimelockExecutionStage { const govInterface = L2ArbitrumGovernor__factory.createInterface(); const proposalStages: L2TimelockExecutionBatchStage[] = []; const timelockInterface = ArbitrumTimelock__factory.createInterface(); + // ids of timelock operations for corresponing ProposalQueued operations found + const timelockOperationIdsFound = new Set(); for (const log of receipt.logs) { if (log.topics.find((t) => t === govInterface.getEventTopic("ProposalQueued"))) { @@ -558,7 +560,6 @@ export class L2TimelockExecutionBatchStage extends L2TimelockExecutionStage { // 10m ~ 1 month on arbitrum const propCreatedStart = log.blockNumber - 10000000; const propCreatedEnd = log.blockNumber; - const propCreatedEvent = await this.getProposalCreatedData( log.address, propQueuedObj.proposalId.toHexString(), @@ -579,12 +580,14 @@ export class L2TimelockExecutionBatchStage extends L2TimelockExecutionStage { constants.HashZero, id(propCreatedEvent.description) ); + const timelockAddress = this.findTimelockAddress(operationId, receipt.logs); if (!timelockAddress) { // if we couldnt find the timelock address it's because the operation id was not found on a callscheduled event // this could be because it was formed via batch instead of single, or vice versa, and is an expected result continue; } + timelockOperationIdsFound.add(operationId); // we know the operation id const executeBatch = new L2TimelockExecutionBatchStage( propCreatedEvent.targets, @@ -596,50 +599,63 @@ export class L2TimelockExecutionBatchStage extends L2TimelockExecutionStage { arbOneSignerOrProvider ); proposalStages.push(executeBatch); - } else if (log.topics[0] === timelockInterface.getEventTopic("CallScheduled")){ - try { + } + + try { + // get calls scheduled directly on timelock (not via gov) + const callScheduledOnTimelock: CallScheduledEvent["args"][] = []; + for (let log of receipt.logs) { + const timelockLog = log.topics.find( + (t) => t === timelockInterface.getEventTopic("CallScheduled") + ); + if (!timelockLog) { + continue; + } const callScheduledArgs = timelockInterface.parseLog(log) .args as CallScheduledEvent["args"]; + // skip calls previously found scheduled via gov + if (timelockOperationIdsFound.has(callScheduledArgs.id)) { + continue; + } + callScheduledOnTimelock.push(callScheduledArgs); + } + + const uniqueOperationIds = new Set(callScheduledOnTimelock.map((arg) => arg.id)).size; + if (uniqueOperationIds == 1) { + // we expect all operations to have the same id (i.e., part of the same batch) + const targets = callScheduledOnTimelock.map((args) => args.target); + const values = callScheduledOnTimelock.map((args) => args[3]); + const datas = callScheduledOnTimelock.map((args) => args.data); + const predecessor = callScheduledOnTimelock[0].predecessor; const { data } = ArbSys__factory.createInterface().decodeFunctionData( "sendTxToL1", - callScheduledArgs.data + datas[0] ); - const { salt } = this.decodeScheduleBatch(data); - - // calculate the id and check if that operation exists - const operationId = this.hashOperationBatch( - [callScheduledArgs.target], - [callScheduledArgs[3]], // cant use .value as ethers fails with this - [callScheduledArgs.data], - callScheduledArgs.predecessor, - salt - ); - - if (operationId !== callScheduledArgs.id) { - throw new Error("Invalid operation id"); + const operationId = this.hashOperationBatch(targets, values, datas, predecessor, salt); + if (operationId !== callScheduledOnTimelock[0].id) { + throw new Error("XXX Invalid operation id"); } - - const timelockAddress = log.address; + const timelockAddress = this.findTimelockAddress(operationId, receipt.logs); + if (!timelockAddress) throw new Error("timelock address not found"); const executeTimelock = new L2TimelockExecutionBatchStage( - [callScheduledArgs.target], - [callScheduledArgs[3]], - [callScheduledArgs.data], - callScheduledArgs.predecessor, + targets, + values, + datas, + predecessor, salt, timelockAddress, arbOneSignerOrProvider ); - if ( - proposalStages.filter((s) => s.identifier === executeTimelock.identifier).length === 0 - ) { - proposalStages.push(executeTimelock); - } - } catch (err) { - // there are expected errors since the calldata may not be of the expected form for decoding - continue; + proposalStages.push(executeTimelock); + } else if (uniqueOperationIds > 1) { + // Multiple calls to scheduleBatch in a single tx is not supported + throw new Error("Multiple batches in single tx"); } + } catch (err) { + // there are expected errors since the calldata may not be of the expected form for decoding + continue; } }