-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.ts
175 lines (153 loc) · 7.34 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
/**
* @notice Entry point for executing a single proposal against a forked mainnet
*/
import dotenv from 'dotenv'
dotenv.config()
import { listFilesInFolder, uploadFileToS3 } from './checks/compound/s3-utils'
import { BigNumber, Contract } from 'ethers'
import { DAO_NAME, GOVERNOR_ADDRESS, SIM_NAME } from './utils/constants'
import { provider } from './utils/clients/ethers'
import { simulate } from './utils/clients/tenderly'
import { AllCheckResults, GovernorType, SimulationConfig, SimulationConfigBase, SimulationData } from './types'
import ALL_CHECKS from './checks'
import { generateAndSaveReports, pushCompoundChecksToDiscord } 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'
/**
* @notice Simulate governance proposals and run proposal checks against them
*/
async function main() {
const s3ReportsFolder = process.env.AWS_BUCKET_BASE_PATH || 'all-proposals'
// --- 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 })
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 allProposalIds = await getProposalIds(governorType, GOVERNOR_ADDRESS, latestBlock.number)
const files = await listFilesInFolder(s3ReportsFolder)
console.log('files', files)
const proposalIdsArr = allProposalIds.filter((id) => id.toNumber() > 228)
const proposalIds = proposalIdsArr.map((id) => BigNumber.from(id))
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) => {
// 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 state = String(states[i]) as keyof typeof PROPOSAL_STATES
const isExecuted = PROPOSAL_STATES[state] === 'Executed'
return { 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`)
const config: SimulationConfig = {
type: simProposal.simType,
daoName: DAO_NAME,
governorAddress: getAddress(GOVERNOR_ADDRESS),
governorType,
proposalId: simProposal.id,
}
const pdfExists = files.includes(`${s3ReportsFolder}/${simProposal.id.toString()}.pdf`)
if (pdfExists) {
console.log(`Skipping simulation for proposal ${simProposal.id} as PDF already exisis in S3...`)
continue
}
console.log(` Simulating ${DAO_NAME} proposal ${simProposal.id}...`)
const { sim, proposal, latestBlock } = await simulate(config)
simOutputs.push({ sim, proposal, latestBlock, config })
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) }
for (const simOutput of simOutputs) {
console.log('Starting proposal checks and report generation...')
// Run checks
const { sim, proposal, latestBlock, config } = simOutput
console.log(` Running for proposal ID ${formatProposalId(governorType, proposal.id!)}...`)
const checksToRun = Object.keys(ALL_CHECKS).filter(
(k) => !process.env.CHECKS_ENABLED || process.env.CHECKS_ENABLED.split(',').includes(k)
)
console.log(`Running ${checksToRun.length} checks: ${checksToRun.join(', ')} for proposal ID ${proposal.id!}`)
const checkResults: AllCheckResults = Object.fromEntries(
await Promise.all(
checksToRun.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
)
await pushCompoundChecksToDiscord(
governorType,
{ start: startBlock, end: endBlock, current: latestBlock },
proposal,
checkResults
)
const reportPath = `reports/${config.daoName}/${config.governorAddress}/${proposal.id}`
await uploadFileToS3(`${s3ReportsFolder}/${proposal.id}.md`, `${reportPath}.md`)
await uploadFileToS3(`${s3ReportsFolder}/${proposal.id}.pdf`, `${reportPath}.pdf`)
await uploadFileToS3(`${s3ReportsFolder}/${proposal.id}.html`, `${reportPath}.html`)
}
console.log('Done!')
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
})