diff --git a/src/govv3/checks/logs.ts b/src/govv3/checks/logs.ts index c2527fa..6229ed6 100644 --- a/src/govv3/checks/logs.ts +++ b/src/govv3/checks/logs.ts @@ -6,7 +6,7 @@ import { getContractName } from '../utils/solidityUtils'; /** * Reports all emitted events from the proposal */ -export const checkLogs: ProposalCheck = { +export const checkLogs: ProposalCheck>> = { name: 'Reports all events emitted from the proposal', async checkProposal(proposal, sim, deps) { let info = []; diff --git a/src/govv3/checks/selfDestruct.ts b/src/govv3/checks/selfDestruct.ts index 6708543..b2e774b 100644 --- a/src/govv3/checks/selfDestruct.ts +++ b/src/govv3/checks/selfDestruct.ts @@ -3,11 +3,12 @@ import { Hex, PublicClient } from 'viem'; import { ProposalCheck } from './types'; import { toAddressLink } from '../utils/markdownUtils'; +import { PayloadsController } from '../payloadsController'; /** * Check all targets with code if they contain selfdestruct. */ -export const checkTargetsNoSelfdestruct: ProposalCheck = { +export const checkTargetsNoSelfdestruct: ProposalCheck>> = { name: 'Check all targets do not contain selfdestruct', async checkProposal(proposal, sim, publicClient) { const allTargets = proposal.payload.actions.map((action) => action.target); @@ -20,7 +21,7 @@ export const checkTargetsNoSelfdestruct: ProposalCheck = { /** * Check all touched contracts with code if they contain selfdestruct. */ -export const checkTouchedContractsNoSelfdestruct: ProposalCheck = { +export const checkTouchedContractsNoSelfdestruct: ProposalCheck = { name: 'Check all touched contracts do not contain selfdestruct', async checkProposal(proposal, sim, publicClient) { const { info, warn, error } = await checkNoSelfdestructs([], sim.transaction.addresses, publicClient); diff --git a/src/govv3/checks/targets-verified.ts b/src/govv3/checks/targets-verified.ts index 6577255..4fd0d1f 100644 --- a/src/govv3/checks/targets-verified.ts +++ b/src/govv3/checks/targets-verified.ts @@ -1,11 +1,12 @@ import { Hex, PublicClient } from 'viem'; import { ProposalCheck } from './types'; import { TenderlySimulationResponse } from '../../utils/tenderlyClient'; +import { PayloadsController } from '../../../dist'; /** * Check all targets with code are verified on Etherscan */ -export const checkTargetsVerifiedEtherscan: ProposalCheck = { +export const checkTargetsVerifiedEtherscan: ProposalCheck>> = { name: 'Check all targets are verified on Etherscan', async checkProposal(proposal, sim, publicClient) { const allTargets = proposal.payload.actions.map((action) => action.target); @@ -18,7 +19,7 @@ export const checkTargetsVerifiedEtherscan: ProposalCheck = { /** * Check all touched contracts with code are verified on Etherscan */ -export const checkTouchedContractsVerifiedEtherscan: ProposalCheck = { +export const checkTouchedContractsVerifiedEtherscan: ProposalCheck = { name: 'Check all touched contracts are verified on Etherscan', async checkProposal(proposal, sim, publicClient) { const info = await checkVerificationStatuses(sim, sim.transaction.addresses, publicClient); diff --git a/src/govv3/checks/types.ts b/src/govv3/checks/types.ts index 81afb4e..b1cd777 100644 --- a/src/govv3/checks/types.ts +++ b/src/govv3/checks/types.ts @@ -8,10 +8,10 @@ export type CheckResult = { errors: string[]; }; -export interface ProposalCheck { +export interface ProposalCheck { name: string; checkProposal( - proposalInfo: Awaited>, + proposalInfo: T, simulation: TenderlySimulationResponse, publicClient: PublicClient ): Promise; diff --git a/src/govv3/generatePayloadReport.ts b/src/govv3/generatePayloadReport.ts index 81a15d1..cb503f6 100644 --- a/src/govv3/generatePayloadReport.ts +++ b/src/govv3/generatePayloadReport.ts @@ -4,7 +4,7 @@ import { PayloadsController } from './payloadsController'; import { tenderlyDeepDiff } from './utils/tenderlyDeepDiff'; import { interpretStateChange } from './utils/stateDiffInterpreter'; import { getContractName } from './utils/solidityUtils'; -import { boolToMarkdown, toTxLink } from './utils/markdownUtils'; +import { boolToMarkdown, renderCheckResult, toTxLink } from './utils/markdownUtils'; import { checkTargetsNoSelfdestruct, checkTouchedContractsNoSelfdestruct } from './checks/selfDestruct'; import { CheckResult, ProposalCheck } from './checks/types'; import { checkLogs } from './checks/logs'; @@ -28,10 +28,10 @@ export async function generateReport({ payloadId, payloadInfo, simulation, publi - actions: ${JSON.stringify(payload.actions, (key, value) => (typeof value === 'bigint' ? value.toString() : value))} - createdAt: [${payload.createdAt}](${toTxLink(createdLog.transactionHash, false, publicClient)})\n`; if (queuedLog) { - report += `- queuedAt: [${payload.createdAt}](${toTxLink(queuedLog.transactionHash, false, publicClient)})\n`; + report += `- queuedAt: [${payload.queuedAt}](${toTxLink(queuedLog.transactionHash, false, publicClient)})\n`; } if (executedLog) { - report += `- executedAt: [${payload.createdAt}](${toTxLink(executedLog.transactionHash, false, publicClient)})\n`; + report += `- executedAt: [${payload.executedAt}](${toTxLink(executedLog.transactionHash, false, publicClient)})\n`; } report += '\n'; @@ -142,11 +142,3 @@ export async function generateReport({ payloadId, payloadInfo, simulation, publi return report; } - -function renderCheckResult(check: ProposalCheck, result: CheckResult) { - let response = `### Check: ${check.name} ${boolToMarkdown(!result.errors.length)}\n\n`; - if (result.errors.length) response += `#### Errors\n\n${result.errors.join('\n')}\n\n`; - if (result.warnings.length) response += `#### Warnings\n\n${result.warnings.join('\n')}\n\n`; - if (result.info.length) response += `#### Info\n\n${result.info.join('\n')}\n\n`; - return response; -} diff --git a/src/govv3/generateProposalReport.ts b/src/govv3/generateProposalReport.ts new file mode 100644 index 0000000..f094f62 --- /dev/null +++ b/src/govv3/generateProposalReport.ts @@ -0,0 +1,143 @@ +import { Hex, PublicClient, getAddress } from 'viem'; +import { StateDiff, TenderlySimulationResponse } from '../utils/tenderlyClient'; +import { PayloadsController } from './payloadsController'; +import { tenderlyDeepDiff } from './utils/tenderlyDeepDiff'; +import { interpretStateChange } from './utils/stateDiffInterpreter'; +import { getContractName } from './utils/solidityUtils'; +import { boolToMarkdown, renderCheckResult, toTxLink } from './utils/markdownUtils'; +import { checkTargetsNoSelfdestruct, checkTouchedContractsNoSelfdestruct } from './checks/selfDestruct'; +import { CheckResult, ProposalCheck } from './checks/types'; +import { checkLogs } from './checks/logs'; +import { checkTargetsVerifiedEtherscan, checkTouchedContractsVerifiedEtherscan } from './checks/targets-verified'; +import { Governance, HUMAN_READABLE_STATE } from './governance'; + +type GenerateReportRequest = { + proposalId: bigint; + proposalInfo: Awaited>; + simulation: TenderlySimulationResponse; + publicClient: PublicClient; +}; + +export async function generateReport({ proposalId, proposalInfo, simulation, publicClient }: GenerateReportRequest) { + const { proposal, executedLog, queuedLog, createdLog, payloadSentLog, votingActivatedLog } = proposalInfo; + // generate file header + let report = `## Proposal ${proposalId} + +- state: ${HUMAN_READABLE_STATE[proposal.state as keyof typeof HUMAN_READABLE_STATE]} +- creator: ${proposal.creator} +- maximumAccessLevelRequired: ${proposal.accessLevel} +- payloads: ${JSON.stringify(proposal.payloads, (key, value) => (typeof value === 'bigint' ? value.toString() : value))} +- createdAt: [${proposal.creationTime}](${toTxLink(createdLog.transactionHash, false, publicClient)})\n`; + if (queuedLog) { + report += `- queuedAt: [${proposal.queuingTime}](${toTxLink(queuedLog.transactionHash, false, publicClient)})\n`; + } + if (executedLog) { + report += `- executedAt: [${executedLog.timestamp}](${toTxLink( + executedLog.transactionHash, + false, + publicClient + )})\n`; + } + report += '\n'; + + // check if simulation was successful + report += `### Simulation ${boolToMarkdown(simulation.transaction.status)}\n\n`; + if (!simulation.transaction.status) { + const txInfo = simulation.transaction.transaction_info; + const reason = txInfo.stack_trace ? txInfo.stack_trace[0].error_reason : 'unknown error'; + report += `Transaction reverted with reason: ${reason}`; + } else { + // State diffs in the simulation are an array, so first we organize them by address. + const stateDiffs = simulation.transaction.transaction_info.state_diff.reduce((diffs, diff) => { + // TODO: double check if that's safe to skip + if (!diff.raw?.[0]) return diffs; + const addr = getAddress(diff.raw[0].address); + if (!diffs[addr]) diffs[addr] = [diff]; + else diffs[addr].push(diff); + return diffs; + }, {} as Record); + + if (!Object.keys(stateDiffs).length) { + report += `No state changes detected`; + } else { + let stateChanges = ''; + let warnings = ''; + // Parse state changes at each address + for (const [address, diffs] of Object.entries(stateDiffs)) { + // Use contracts array to get contract name of address + stateChanges += `\n\`\`\`diff\n# ${getContractName(simulation.contracts, address)}\n`; + + // Parse each diff. A single diff may involve multiple storage changes, e.g. a proposal that + // executes three transactions will show three state changes to the `queuedTransactions` + // mapping within a single `diff` element. We always JSON.stringify the values so structs + // (i.e. tuples) don't print as [object Object] + for (const diff of diffs) { + if (!diff.soltype) { + // In this branch, state change is not decoded, so return raw data of each storage write + // (all other branches have decoded state changes) + diff.raw.forEach((w) => { + const oldVal = JSON.stringify(w.original); + const newVal = JSON.stringify(w.dirty); + // info += `\n - Slot \`${w.key}\` changed from \`${oldVal}\` to \`${newVal}\`` + stateChanges += tenderlyDeepDiff(oldVal, newVal, `Slot \`${w.key}\``); + }); + } else if (diff.soltype.simple_type) { + // This is a simple type with a single changed value + // const oldVal = JSON.parse(JSON.stringify(diff.original)) + // const newVal = JSON.parse(JSON.stringify(diff.dirty)) + // info += `\n - \`${diff.soltype.name}\` changed from \`${oldVal}\` to \`${newVal}\`` + stateChanges += tenderlyDeepDiff(diff.original, diff.dirty, diff.soltype.name); + } else if (diff.soltype.type.startsWith('mapping')) { + // This is a complex type like a mapping, which may have multiple changes. The diff.original + // and diff.dirty fields can be strings or objects, and for complex types they are objects, + // so we cast them as such + const keys = Object.keys(diff.original); + const original = diff.original as Record; + const dirty = diff.dirty as Record; + for (const k of keys as Hex[]) { + stateChanges += tenderlyDeepDiff(original[k], dirty[k], `\`${diff.soltype?.name}\` key \`${k}\``); + const interpretation = await interpretStateChange( + address, + diff.soltype?.name, + original[k], + dirty[k], + k, + publicClient + ); + if (interpretation) stateChanges += `\n${interpretation}`; + stateChanges += '\n'; + } + } else { + // TODO arrays and nested mapping are currently not well supported -- find a transaction + // that changes state of these types to inspect the Tenderly simulation response and + // handle it accordingly. In the meantime we show the raw state changes and print a + // warning about decoding the data + diff.raw.forEach((w) => { + const oldVal = JSON.stringify(w.original); + const newVal = JSON.stringify(w.dirty); + // info += `\n - Slot \`${w.key}\` changed from \`${oldVal}\` to \`${newVal}\`` + stateChanges += tenderlyDeepDiff(oldVal, newVal, `Slot \`${w.key}\``); + warnings += `Could not parse state: add support for formatting type ${diff.soltype?.type} (slot ${w.key})\n`; + }); + } + } + stateChanges += '```\n'; + } + + if (warnings) { + report += `#### Warnings\n`; + report += warnings; + } + report += `#### State Changes\n`; + report += stateChanges; + } + } + const checks = [checkLogs, checkTouchedContractsVerifiedEtherscan, checkTouchedContractsNoSelfdestruct]; + + for (const check of checks) { + const result = await check.checkProposal(proposalInfo, simulation, publicClient); + report += renderCheckResult(check, result); + } + + return report; +} diff --git a/src/govv3/utils/markdownUtils.ts b/src/govv3/utils/markdownUtils.ts index 31c9c12..d21cc73 100644 --- a/src/govv3/utils/markdownUtils.ts +++ b/src/govv3/utils/markdownUtils.ts @@ -1,4 +1,5 @@ import { Hex, PublicClient } from 'viem'; +import { CheckResult } from '../checks/types'; export function boolToMarkdown(value: boolean) { if (value) return `:white_check_mark:`; @@ -24,3 +25,11 @@ export function toTxLink(txn: Hex, md: boolean, client: PublicClient): string { if (md) return `[${txn}](${link})`; return link; } + +export function renderCheckResult(check: { name: string }, result: CheckResult) { + let response = `### Check: ${check.name} ${boolToMarkdown(!result.errors.length)}\n\n`; + if (result.errors.length) response += `#### Errors\n\n${result.errors.join('\n')}\n\n`; + if (result.warnings.length) response += `#### Warnings\n\n${result.warnings.join('\n')}\n\n`; + if (result.info.length) response += `#### Info\n\n${result.info.join('\n')}\n\n`; + return response; +}