|
From 000de957f54b25be84619a91919495dae7c5a9a7 Mon Sep 17 00:00:00 2001 |
|
From: Matt Solomon <matt@mattsolomon.dev> |
|
Date: Thu, 10 Aug 2023 09:20:35 -0700 |
|
Subject: [PATCH] feat: add support for Governor Alpha, the Gitcoin governor |
|
|
|
There is at least one spot where the logic is hardcoded to be specific |
|
to Gitcoin, where we call the gtc method to get the voting token |
|
address. This will need to be changed if we want to support other |
|
GovernorAlpha instances. Also note this commit overrides the normal |
|
behavior to only run proposal 65 |
|
--- |
|
checks/check-targets-no-selfdestruct.ts | 2 +- |
|
index.ts | 1 + |
|
types.d.ts | 2 +- |
|
utils/clients/tenderly.ts | 23 ++++- |
|
utils/contracts/governor-alpha.ts | 106 ++++++++++++++++++++++++ |
|
utils/contracts/governor.ts | 17 ++++ |
|
6 files changed, 145 insertions(+), 6 deletions(-) |
|
create mode 100644 utils/contracts/governor-alpha.ts |
|
|
|
diff --git a/checks/check-targets-no-selfdestruct.ts b/checks/check-targets-no-selfdestruct.ts |
|
index aec340b..1787be2 100644 |
|
--- a/checks/check-targets-no-selfdestruct.ts |
|
+++ b/checks/check-targets-no-selfdestruct.ts |
|
@@ -78,7 +78,7 @@ async function checkNoSelfdestruct( |
|
addr: string, |
|
provider: JsonRpcProvider |
|
): Promise<'safe' | 'eoa' | 'empty' | 'selfdestruct' | 'delegatecall' | 'trusted'> { |
|
- if (trustedAddrs.map(addr => addr.toLowerCase()).includes(addr.toLowerCase())) return 'trusted' |
|
+ if (trustedAddrs.map((addr) => addr.toLowerCase()).includes(addr.toLowerCase())) return 'trusted' |
|
|
|
const [code, nonce] = await Promise.all([provider.getCode(addr), provider.getTransactionCount(addr)]) |
|
|
|
diff --git a/index.ts b/index.ts |
|
index b54c318..976fabe 100644 |
|
--- a/index.ts |
|
+++ b/index.ts |
|
@@ -78,6 +78,7 @@ async function main() { |
|
) |
|
|
|
for (const simProposal of simProposals) { |
|
+ if (simProposal.id.toNumber() != 65) continue |
|
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}...`) |
|
diff --git a/types.d.ts b/types.d.ts |
|
index 9ecc722..0191244 100644 |
|
--- a/types.d.ts |
|
+++ b/types.d.ts |
|
@@ -4,7 +4,7 @@ import { JsonRpcProvider } from '@ethersproject/providers' |
|
|
|
// --- Simulation configurations --- |
|
// TODO Consider refactoring to an enum instead of string. |
|
-export type GovernorType = 'oz' | 'bravo' |
|
+export type GovernorType = 'oz' | 'bravo' | 'alpha' |
|
|
|
interface SimulationConfigBase { |
|
type: 'executed' | 'proposed' | 'new' |
|
diff --git a/utils/clients/tenderly.ts b/utils/clients/tenderly.ts |
|
index 0e2f442..6b28628 100644 |
|
--- a/utils/clients/tenderly.ts |
|
+++ b/utils/clients/tenderly.ts |
|
@@ -296,7 +296,10 @@ async function simulateProposed(config: SimulationConfigProposed): Promise<Simul |
|
|
|
// For Bravo governors, we use the block right after the proposal ends, and for OZ |
|
// governors we arbitrarily use the next block number. |
|
- const simBlock = governorType === 'bravo' ? proposal.endBlock!.add(1) : BigNumber.from(latestBlock.number + 1) |
|
+ const simBlock = |
|
+ governorType === 'bravo' || governorType === 'alpha' |
|
+ ? proposal.endBlock!.add(1) |
|
+ : BigNumber.from(latestBlock.number + 1) |
|
|
|
// For OZ governors we are given the earliest possible execution time. For Bravo governors, we |
|
// Compute the approximate earliest possible execution time based on governance parameters. This |
|
@@ -306,7 +309,7 @@ async function simulateProposed(config: SimulationConfigProposed): Promise<Simul |
|
// proposals call methods that pass in a start timestamp that must be lower than the current |
|
// block timestamp (represented by the `simTimestamp` variable below) |
|
const simTimestamp = |
|
- governorType === 'bravo' |
|
+ governorType === 'bravo' || governorType === 'alpha' |
|
? BigNumber.from(latestBlock.timestamp).add(simBlock.sub(proposal.endBlock!).mul(12)) |
|
: proposal.endTime!.add(1) |
|
const eta = simTimestamp // set proposal eta to be equal to the timestamp we simulate at |
|
@@ -332,7 +335,17 @@ async function simulateProposed(config: SimulationConfigProposed): Promise<Simul |
|
|
|
const proposalIdBn = BigNumber.from(proposalId) |
|
let governorStateOverrides: Record<string, string> = {} |
|
- if (governorType === 'bravo') { |
|
+ if (governorType === 'alpha') { |
|
+ const proposalKey = `proposals[${proposalIdBn.toString()}]` |
|
+ governorStateOverrides = { |
|
+ proposalCount: proposalId.toString(), |
|
+ [`${proposalKey}.eta`]: eta.toString(), |
|
+ [`${proposalKey}.canceled`]: 'false', |
|
+ [`${proposalKey}.executed`]: 'false', |
|
+ [`${proposalKey}.forVotes`]: votingTokenSupply.toString(), |
|
+ [`${proposalKey}.againstVotes`]: '0', |
|
+ } |
|
+ } else if (governorType === 'bravo') { |
|
const proposalKey = `proposals[${proposalIdBn.toString()}]` |
|
governorStateOverrides = { |
|
proposalCount: proposalId.toString(), |
|
@@ -377,7 +390,9 @@ async function simulateProposed(config: SimulationConfigProposed): Promise<Simul |
|
// ensure Tenderly properly parses the simulation payload |
|
const descriptionHash = keccak256(toUtf8Bytes(description)) |
|
const executeInputs = |
|
- governorType === 'bravo' ? [proposalId.toString()] : [targets, values, calldatas, descriptionHash] |
|
+ governorType === 'bravo' || governorType === 'alpha' |
|
+ ? [proposalId.toString()] |
|
+ : [targets, values, calldatas, descriptionHash] |
|
|
|
let simulationPayload: TenderlyPayload = { |
|
network_id: '1', |
|
diff --git a/utils/contracts/governor-alpha.ts b/utils/contracts/governor-alpha.ts |
|
new file mode 100644 |
|
index 0000000..c149e9c |
|
--- /dev/null |
|
+++ b/utils/contracts/governor-alpha.ts |
|
@@ -0,0 +1,106 @@ |
|
+import { BigNumber, BigNumberish, Contract } from 'ethers' |
|
+import { hexZeroPad } from '@ethersproject/bytes' |
|
+import { provider } from '../clients/ethers' |
|
+import { getSolidityStorageSlotUint, to32ByteHexString } from '../utils' |
|
+ |
|
+const GOVERNOR_ALPHA_ABI = [ |
|
+ 'constructor(address timelock_, address gtc_)', |
|
+ 'event ProposalCanceled(uint256 id)', |
|
+ 'event ProposalCreated(uint256 id, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description)', |
|
+ 'event ProposalExecuted(uint256 id)', |
|
+ 'event ProposalQueued(uint256 id, uint256 eta)', |
|
+ 'event VoteCast(address voter, uint256 proposalId, bool support, uint256 votes)', |
|
+ 'function BALLOT_TYPEHASH() view returns (bytes32)', |
|
+ 'function DOMAIN_TYPEHASH() view returns (bytes32)', |
|
+ 'function cancel(uint256 proposalId)', |
|
+ 'function castVote(uint256 proposalId, bool support)', |
|
+ 'function castVoteBySig(uint256 proposalId, bool support, uint8 v, bytes32 r, bytes32 s)', |
|
+ 'function execute(uint256 proposalId) payable', |
|
+ 'function getActions(uint256 proposalId) view returns (address[] targets, uint256[] values, string[] signatures, bytes[] calldatas)', |
|
+ 'function getReceipt(uint256 proposalId, address voter) view returns (tuple(bool hasVoted, bool support, uint96 votes))', |
|
+ 'function gtc() view returns (address)', |
|
+ 'function latestProposalIds(address) view returns (uint256)', |
|
+ 'function name() view returns (string)', |
|
+ 'function proposalCount() view returns (uint256)', |
|
+ 'function proposalMaxOperations() pure returns (uint256)', |
|
+ 'function proposalThreshold() pure returns (uint256)', |
|
+ 'function proposals(uint256) view returns (uint256 id, address proposer, uint256 eta, uint256 startBlock, uint256 endBlock, uint256 forVotes, uint256 againstVotes, bool canceled, bool executed)', |
|
+ 'function propose(address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, string description) returns (uint256)', |
|
+ 'function queue(uint256 proposalId)', |
|
+ 'function quorumVotes() pure returns (uint256)', |
|
+ 'function state(uint256 proposalId) view returns (uint8)', |
|
+ 'function timelock() view returns (address)', |
|
+ 'function votingDelay() pure returns (uint256)', |
|
+ 'function votingPeriod() pure returns (uint256)', |
|
+] |
|
+ |
|
+export const governorAlpha = (address: string) => new Contract(address, GOVERNOR_ALPHA_ABI, provider) |
|
+ |
|
+// All possible states a proposal might be in. |
|
+// These are defined by the `ProposalState` enum so when we fetch the state of a proposal ID |
|
+// we receive an integer response, and use this to map that integer to the state |
|
+export const PROPOSAL_STATES = { |
|
+ '0': 'Pending', |
|
+ '1': 'Active', |
|
+ '2': 'Canceled', |
|
+ '3': 'Defeated', |
|
+ '4': 'Succeeded', |
|
+ '5': 'Queued', |
|
+ '6': 'Expired', |
|
+ '7': 'Executed', |
|
+} |
|
+ |
|
+/** |
|
+ * @notice Returns an object containing various GovernorAlpha slots |
|
+ * @param id Proposal ID |
|
+ */ |
|
+export function getAlphaSlots(proposalId: BigNumberish) { |
|
+ // Proposal struct slot offsets, based on the governor's proposal struct |
|
+ // struct Proposal { |
|
+ // uint id; |
|
+ // address proposer; |
|
+ // uint eta; |
|
+ // address[] targets; |
|
+ // uint[] values; |
|
+ // string[] signatures; |
|
+ // bytes[] calldatas; |
|
+ // uint startBlock; |
|
+ // uint endBlock; |
|
+ // uint forVotes; |
|
+ // uint againstVotes; |
|
+ // uint abstainVotes; |
|
+ // bool canceled; |
|
+ // bool executed; |
|
+ // mapping (address => Receipt) receipts; |
|
+ // } |
|
+ const etaOffset = 2 |
|
+ const targetsOffset = 3 |
|
+ const valuesOffset = 4 |
|
+ const signaturesOffset = 5 |
|
+ const calldatasOffset = 6 |
|
+ const forVotesOffset = 9 |
|
+ const againstVotesOffset = 10 |
|
+ const abstainVotesOffset = 11 |
|
+ const canceledSlotOffset = 12 // this is packed with `executed` |
|
+ |
|
+ // Compute and return slot numbers |
|
+ const proposalsMapSlot = '0xa' // proposals ID to proposal struct mapping |
|
+ const proposalSlot = getSolidityStorageSlotUint(proposalsMapSlot, proposalId) |
|
+ |
|
+ throw new Error('TODO: implement getAlphaSlots') |
|
+ // return { |
|
+ // proposalCount: to32ByteHexString('0x7'), // slot of the proposalCount storage variable |
|
+ // votingToken: '0x9', // slot of voting token, e.g. UNI, COMP (getter is named after token, so can't generalize it that way), |
|
+ // proposalsMap: proposalsMapSlot, |
|
+ // proposal: proposalSlot, |
|
+ // canceled: hexZeroPad(BigNumber.from(proposalSlot).add(canceledSlotOffset).toHexString(), 32), |
|
+ // eta: hexZeroPad(BigNumber.from(proposalSlot).add(etaOffset).toHexString(), 32), |
|
+ // forVotes: hexZeroPad(BigNumber.from(proposalSlot).add(forVotesOffset).toHexString(), 32), |
|
+ // againstVotes: hexZeroPad(BigNumber.from(proposalSlot).add(againstVotesOffset).toHexString(), 32), |
|
+ // abstainVotes: hexZeroPad(BigNumber.from(proposalSlot).add(abstainVotesOffset).toHexString(), 32), |
|
+ // targets: hexZeroPad(BigNumber.from(proposalSlot).add(targetsOffset).toHexString(), 32), |
|
+ // values: hexZeroPad(BigNumber.from(proposalSlot).add(valuesOffset).toHexString(), 32), |
|
+ // signatures: hexZeroPad(BigNumber.from(proposalSlot).add(signaturesOffset).toHexString(), 32), |
|
+ // calldatas: hexZeroPad(BigNumber.from(proposalSlot).add(calldatasOffset).toHexString(), 32), |
|
+ // } |
|
+} |
|
diff --git a/utils/contracts/governor.ts b/utils/contracts/governor.ts |
|
index b45cacb..4da9d23 100644 |
|
--- a/utils/contracts/governor.ts |
|
+++ b/utils/contracts/governor.ts |
|
@@ -4,6 +4,7 @@ import { toUtf8Bytes } from '@ethersproject/strings' |
|
import { keccak256 } from '@ethersproject/keccak256' |
|
import { defaultAbiCoder } from '@ethersproject/abi' |
|
import { provider } from '../clients/ethers' |
|
+import { governorAlpha, getAlphaSlots } from './governor-alpha' |
|
import { governorBravo, getBravoSlots } from './governor-bravo' |
|
import { governorOz, getOzSlots } from './governor-oz' |
|
import { timelock } from './timelock' |
|
@@ -11,6 +12,8 @@ import { GovernorType, ProposalEvent, ProposalStruct } from '../../types' |
|
|
|
// --- Exported methods --- |
|
export async function inferGovernorType(address: string): Promise<GovernorType> { |
|
+ if (address === '0xDbD27635A534A3d3169Ef0498beB56Fb9c937489') return 'alpha' |
|
+ |
|
const abi = ['function initialProposalId() external view returns (uint256)'] |
|
const governor = new Contract(address, abi, provider) |
|
|
|
@@ -26,6 +29,7 @@ export async function inferGovernorType(address: string): Promise<GovernorType> |
|
} |
|
|
|
export function getGovernor(governorType: GovernorType, address: string) { |
|
+ if (governorType === 'alpha') return governorAlpha(address) |
|
if (governorType === 'bravo') return governorBravo(address) |
|
if (governorType === 'oz') return governorOz(address) |
|
throw new Error(`Unknown governor type: ${governorType}`) |
|
@@ -37,6 +41,7 @@ export async function getProposal( |
|
proposalId: BigNumberish |
|
): Promise<ProposalStruct> { |
|
const governor = getGovernor(governorType, address) |
|
+ if (governorType === 'alpha') return governor.proposals(proposalId) |
|
if (governorType === 'bravo') return governor.proposals(proposalId) |
|
|
|
// Piece together the struct for OZ Governors. |
|
@@ -63,12 +68,14 @@ export async function getProposal( |
|
|
|
export async function getTimelock(governorType: GovernorType, address: string) { |
|
const governor = getGovernor(governorType, address) |
|
+ if (governorType === 'alpha') return timelock(await governor.timelock()) |
|
if (governorType === 'bravo') return timelock(await governor.admin()) |
|
return timelock(await governor.timelock()) |
|
} |
|
|
|
export async function getVotingToken(governorType: GovernorType, address: string, proposalId: BigNumberish) { |
|
const governor = getGovernor(governorType, address) |
|
+ if (governorType === 'alpha') return erc20(await governor.gtc()) // Only supports gitcoin |
|
if (governorType === 'bravo') { |
|
// Get voting token and total supply |
|
const govSlots = getBravoSlots(proposalId) |
|
@@ -81,6 +88,7 @@ export async function getVotingToken(governorType: GovernorType, address: string |
|
} |
|
|
|
export function getGovSlots(governorType: GovernorType, proposalId: BigNumberish) { |
|
+ if (governorType === 'alpha') return getAlphaSlots(proposalId) |
|
if (governorType === 'bravo') return getBravoSlots(proposalId) |
|
return getOzSlots(proposalId) |
|
} |
|
@@ -90,6 +98,14 @@ export async function getProposalIds( |
|
address: string, |
|
latestBlockNum: number |
|
): Promise<BigNumber[]> { |
|
+ if (governorType === 'alpha') { |
|
+ // Fetch all proposal IDs |
|
+ const governor = governorAlpha(address) |
|
+ const proposalCreatedLogs = await governor.queryFilter(governor.filters.ProposalCreated(), 0, latestBlockNum) |
|
+ const allProposalIds = proposalCreatedLogs.map((logs) => (logs.args as unknown as ProposalEvent).id!) |
|
+ return allProposalIds |
|
+ } |
|
+ |
|
if (governorType === 'bravo') { |
|
// Fetch all proposal IDs |
|
const governor = governorBravo(address) |
|
@@ -129,6 +145,7 @@ export async function generateProposalId( |
|
description: '', |
|
} |
|
): Promise<BigNumber> { |
|
+ if (governorType === 'alpha') throw new Error('generateProposalId not supported for GovernorAlpha') |
|
// Fetch proposal count from the contract and increment it by 1. |
|
if (governorType === 'bravo') { |
|
const count: BigNumber = await governorBravo(address).proposalCount() |
|
-- |
|
2.39.2 (Apple Git-143) |
|
|