forked from Uniswap/governance-seatbelt
-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.ts
301 lines (276 loc) · 12.7 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/**
* @notice Entry point for executing a single proposal against a forked mainnet
*/
import dotenv from 'dotenv'
dotenv.config()
import { BigNumber, BigNumberish, Contract, constants } from 'ethers'
import { DAO_NAME, GOVERNOR_ADDRESS, SIM_NAME } from './utils/constants'
import { arb1provider, novaprovider, l1provider, provider } from './utils/clients/ethers'
import { simulate } from './utils/clients/tenderly'
import {
AllCheckResults,
GovernorType,
SimulationConfig,
SimulationConfigBase,
SimulationConfigArbL2ToL1,
SimulationData,
SimulationResult,
SimulationConfigArbRetryable,
} from './types'
import ALL_CHECKS from './checks'
import { generateAndSaveReports } from './presentation/report'
import { PROPOSAL_STATES } from './utils/contracts/governor-bravo'
import {
formatProposalId,
getGovernor,
getProposalIds,
getTimelock,
inferGovernorType,
} from './utils/contracts/governor'
import { getAddress } from '@ethersproject/address'
import { SubmitRetryableMessageDataParser } from '@arbitrum/sdk/dist/lib/message/messageDataParser'
import { Inbox__factory } from '@arbitrum/sdk/dist/lib/abi/factories/Inbox__factory'
import { Bridge__factory } from '@arbitrum/sdk/dist/lib/abi/factories/Bridge__factory'
import { ArbSys__factory } from '@arbitrum/sdk/dist/lib/abi/factories/ArbSys__factory'
import { EventArgs, parseTypedLogs } from '@arbitrum/sdk/dist/lib/dataEntities/event'
import { InboxMessageDeliveredEvent } from '@arbitrum/sdk/dist/lib/abi/Inbox'
import { MessageDeliveredEvent } from '@arbitrum/sdk/dist/lib/abi/Bridge'
import { InboxMessageKind } from '@arbitrum/sdk/dist/lib/dataEntities/message'
import { getL2Network } from '@arbitrum/sdk'
// This function find L2ToL1 events in a simulation result and create a new simulation for each of them
async function simL2toL1(sr: SimulationResult, simname: string) {
const { proposal, sim } = sr
const parentId = proposal.id!
const rawlog = sim.transaction.transaction_info.logs?.map((l) => l.raw)
if(!rawlog) return []
const L2ToL1TxEvents = parseTypedLogs(ArbSys__factory, rawlog as any, 'L2ToL1Tx')
const simresults = []
let offset = 1
for (const l2ToL1TxEvent of L2ToL1TxEvents) {
const l2tol1config: SimulationConfigArbL2ToL1 = {
type: 'arbl2tol1',
daoName: simname,
governorType: 'arb',
governorAddress: getAddress(GOVERNOR_ADDRESS || constants.AddressZero),
targets: [l2ToL1TxEvent.destination], // Array of targets to call.
values: [l2ToL1TxEvent.callvalue], // Array of values with each call.
signatures: [''], // Array of function signatures. Leave empty if generating calldata with ethers like we do here.
calldatas: [l2ToL1TxEvent.data], // Array of encoded calldatas.
description: `# This is a L1 Timelock Execution of simulation ${parentId.toString()}\n .`,
parentId: parentId.div(10000000).add(1).mul(10000000),
idoffset: offset,
}
offset += 1000000 // reserve spaces for retryable exections
const { sim, proposal, latestBlock } = await simulate(l2tol1config)
simresults.push({ sim, proposal, latestBlock, config: l2tol1config })
}
return simresults
}
// This function find retryable in a simulation result and create a new simulation for each of them
async function simRetryable(sr: SimulationResult, simname: string) {
const { proposal, sim } = sr
const parentId = proposal.id!
const rawlog = sim.transaction.transaction_info.logs?.map((l) => l.raw)
if(!rawlog) return []
const bridgeMessages = parseTypedLogs(Bridge__factory, rawlog as any, 'MessageDelivered')
const inboxMessages = parseTypedLogs(Inbox__factory, rawlog as any, 'InboxMessageDelivered(uint256,bytes)')
if (bridgeMessages.length !== inboxMessages.length) {
throw new Error('Unexpected number of message delivered events')
}
// TODO: Only Arb1 is supported right now
const messages: {
inboxMessageEvent: EventArgs<InboxMessageDeliveredEvent>
bridgeMessageEvent: EventArgs<MessageDeliveredEvent>
chainId: 42161 | 42170
}[] = []
const arb1Inbox = (await getL2Network(arb1provider as any)).ethBridge.inbox.toLowerCase()
const novaInbox = (await getL2Network(novaprovider as any)).ethBridge.inbox.toLowerCase()
for (const bm of bridgeMessages) {
const chainId = bm.inbox.toLowerCase() === arb1Inbox ? 42161 : bm.inbox.toLowerCase() === novaInbox ? 42170 : 0
if (chainId === 0) continue // unknown inbox
const im = inboxMessages.filter((i) => i.messageNum.eq(bm.messageIndex))[0]
if (!im) {
throw new Error(
`Unexepected missing event for message index: ${bm.messageIndex.toString()}. ${JSON.stringify(inboxMessages)}`
)
}
messages.push({
inboxMessageEvent: im,
bridgeMessageEvent: bm,
chainId: chainId,
})
}
const simresults = []
let offset = 10000
for (const { inboxMessageEvent, bridgeMessageEvent, chainId } of messages) {
if (bridgeMessageEvent.kind === InboxMessageKind.L1MessageType_submitRetryableTx) {
const parser = new SubmitRetryableMessageDataParser()
const parsedRetryable = parser.parse(inboxMessageEvent.data)
const l2tol1config: SimulationConfigArbRetryable = {
type: 'arbretryable',
from: bridgeMessageEvent.sender,
daoName: simname,
governorType: 'arb',
governorAddress: getAddress(GOVERNOR_ADDRESS || constants.AddressZero),
targets: [parsedRetryable.destAddress], // Array of targets to call.
values: [parsedRetryable.l2CallValue], // Array of values with each call.
signatures: [''], // Array of function signatures. Leave empty if generating calldata with ethers like we do here.
calldatas: [parsedRetryable.data], // Array of encoded calldatas.
description: `# This is a L2(${chainId}) Retryable Execution of simulation ${parentId.toString()} \n.`,
parentId: parentId,
idoffset: offset,
chainId: chainId
}
offset += 10000
const { sim, proposal, latestBlock } = await simulate(l2tol1config)
simresults.push({ sim, proposal, latestBlock, config: l2tol1config })
}
}
return simresults
}
/**
* @notice Simulate governance proposals and run proposal checks against them
*/
async function main() {
// --- Run simulations ---
// Prepare array to store all simulation outputs
const simOutputs: SimulationData[] = []
let governor: Contract
let governorType: GovernorType
// Determine if we are running a specific simulation or all on-chain proposals for a specified governor.
if (SIM_NAME) {
// If a SIM_NAME is provided, we run that simulation
const configPath = `./sims/${SIM_NAME}.sim.ts`
const config: SimulationConfig = await import(configPath).then((d) => d.config) // dynamic path `import` statements not allowed
const { sim, proposal, latestBlock } = await simulate(config)
simOutputs.push({ sim, proposal, latestBlock, config })
if ((config.type === 'new' || config.type === 'proposed') && config.governorType === 'arb') {
const l2tol1sims = await simL2toL1({ sim, proposal, latestBlock }, config.daoName)
for (const l2tol1sim of l2tol1sims) {
simOutputs.push(l2tol1sim!)
const retryablesims = await simRetryable(
{ sim: l2tol1sim!.sim, proposal: l2tol1sim!.proposal, latestBlock: l2tol1sim!.latestBlock },
config.daoName
)
for (const retryablesim of retryablesims) {
simOutputs.push(retryablesim!)
}
}
} else {
const retryablesims = await simRetryable({ sim, proposal, latestBlock }, config.daoName)
for (const retryablesim of retryablesims) {
simOutputs.push(retryablesim!)
}
}
governorType = await inferGovernorType(config.governorAddress)
governor = await getGovernor(governorType, config.governorAddress)
} else {
// If no SIM_NAME is provided, we get proposals to simulate from the chain
if (!GOVERNOR_ADDRESS) throw new Error('Must provider a GOVERNOR_ADDRESS')
if (!DAO_NAME) throw new Error('Must provider a DAO_NAME')
const latestBlock = await provider.getBlock('latest')
// Fetch all proposal IDs
governorType = await inferGovernorType(GOVERNOR_ADDRESS)
const proposalIds = await getProposalIds(governorType, GOVERNOR_ADDRESS, latestBlock.number)
governor = getGovernor(governorType, GOVERNOR_ADDRESS)
// If we aren't simulating all proposals, filter down to just the active ones. For now we
// assume we're simulating all by default
const states = await Promise.all(proposalIds.map((id) => governor.state(id)))
const simProposals: { id: BigNumber; simType: SimulationConfigBase['type'] }[] = proposalIds.map((id, i) => {
const state = String(states[i]) as keyof typeof PROPOSAL_STATES
const proposalState = PROPOSAL_STATES[state]
return { id, proposalState}
}).filter(p => {
return !process.env.ONLY_RELEVANT ||
p.proposalState === 'Pending' ||
p.proposalState === 'Active' ||
p.proposalState === 'Queued' ||
p.proposalState === 'Succeeded'
}).map(p => {
// If state is `Executed` (state 7), we use the executed sim type and effectively just
// simulate the real transaction. For all other states, we use the `proposed` type because
// state overrides are required to simulate the transaction
const isExecuted = p.proposalState === 'Executed'
return { id: p.id, simType: isExecuted ? 'executed' : 'proposed' }
})
const simProposalsIds = simProposals.map((sim) => sim.id)
// Simulate them
// We intentionally do not run these in parallel to avoid hitting Tenderly API rate limits or flooding
// them with requests if we e.g. simulate all proposals for a governor (instead of just active ones)
const numProposals = simProposals.length
console.log(
`Simulating ${numProposals} ${DAO_NAME} proposals: IDs of ${simProposalsIds
.map((id) => formatProposalId(governorType, id))
.join(', ')}`
)
for (const simProposal of simProposals) {
if (simProposal.simType === 'new') throw new Error('Simulation type "new" is not supported in this branch')
// Determine if this proposal is already `executed` or currently in-progress (`proposed`)
console.log(` Simulating ${DAO_NAME} proposal ${simProposal.id} ...`)
const config: SimulationConfig = {
type: simProposal.simType,
daoName: DAO_NAME,
governorAddress: getAddress(GOVERNOR_ADDRESS),
governorType,
proposalId: simProposal.id,
}
const { sim, proposal, latestBlock } = await simulate(config)
simOutputs.push({ sim, proposal, latestBlock, config })
const l2tol1sims = await simL2toL1({ sim, proposal, latestBlock }, config.daoName)
for (const l2tol1sim of l2tol1sims) {
simOutputs.push(l2tol1sim!)
const retryablesims = await simRetryable(
{ sim: l2tol1sim!.sim, proposal: l2tol1sim!.proposal, latestBlock: l2tol1sim!.latestBlock },
config.daoName
)
for (const retryablesim of retryablesims) {
simOutputs.push(retryablesim!)
}
}
console.log(` done`)
}
}
// --- Run proposal checks and save output ---
// Generate the proposal data and dependencies needed by checks
const proposalData = { governor, provider, timelock: await getTimelock(governorType, governor.address) }
console.log('Starting proposal checks and report generation...')
for (const simOutput of simOutputs) {
// Run checks
const { sim, proposal, latestBlock, config } = simOutput
console.log(` Running for proposal ID ${formatProposalId(governorType, proposal.id!)} ...`)
const checkResults: AllCheckResults = Object.fromEntries(
await Promise.all(
Object.keys(ALL_CHECKS).map(async (checkId) => [
checkId,
{
name: ALL_CHECKS[checkId].name,
result: await ALL_CHECKS[checkId].checkProposal(proposal, sim, proposalData),
},
])
)
)
// Generate markdown report.
const [startBlock, endBlock] = await Promise.all([
proposal.startBlock.toNumber() <= latestBlock.number ? provider.getBlock(proposal.startBlock.toNumber()) : null,
proposal.endBlock.toNumber() <= latestBlock.number ? provider.getBlock(proposal.endBlock.toNumber()) : null,
])
// Save markdown report to a file.
// GitHub artifacts are flattened (folder structure is not preserved), so we include the DAO name in the filename.
const dir = `./reports/${config.daoName}/${config.governorAddress}`
await generateAndSaveReports(
governorType,
{ start: startBlock, end: endBlock, current: latestBlock },
proposal,
checkResults,
dir,
sim.simulation.id
)
}
console.log('Done!')
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
})