feat: add multi-contract governance support (#1860)

* Revert "feat: quick fix for new governor"
This reverts commit 5dd1249dddc3e097c26a68bd5b4157ffa341dd60.

* support multiple governance contracts
This commit is contained in:
Jordan Frankfurt 2021-06-15 22:36:27 -04:00 committed by GitHub
parent 1b27d8dab0
commit 014595cdfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 281 additions and 169 deletions

@ -1,33 +1,29 @@
import useScrollPosition from '@react-hook/window-scroll'
import React, { useState } from 'react'
import { Text } from 'rebass'
import { NavLink } from 'react-router-dom'
import { darken } from 'polished'
import { Trans } from '@lingui/macro'
import useScrollPosition from '@react-hook/window-scroll'
import { darken } from 'polished'
import React, { useState } from 'react'
import { Moon, Sun } from 'react-feather'
import { NavLink } from 'react-router-dom'
import { Text } from 'rebass'
import { useShowClaimPopup, useToggleSelfClaimModal } from 'state/application/hooks'
import { useUserHasAvailableClaim } from 'state/claim/hooks'
import { useUserHasSubmittedClaim } from 'state/transactions/hooks'
import { useDarkModeManager } from 'state/user/hooks'
import { useETHBalances } from 'state/wallet/hooks'
import styled from 'styled-components/macro'
import Logo from '../../assets/svg/logo.svg'
import LogoDark from '../../assets/svg/logo_white.svg'
import { SupportedChainId } from '../../constants/chains'
import { NETWORK_LABELS, SupportedChainId } from '../../constants/chains'
import { useActiveWeb3React } from '../../hooks/web3'
import { useDarkModeManager } from '../../state/user/hooks'
import { useETHBalances } from '../../state/wallet/hooks'
import { CardNoise } from '../earn/styled'
import { TYPE, ExternalLink } from '../../theme'
import { ExternalLink, TYPE } from '../../theme'
import { YellowCard } from '../Card'
import Menu from '../Menu'
import Row, { RowFixed } from '../Row'
import Web3Status from '../Web3Status'
import ClaimModal from '../claim/ClaimModal'
import { useToggleSelfClaimModal, useShowClaimPopup } from '../../state/application/hooks'
import { useUserHasAvailableClaim } from '../../state/claim/hooks'
import { useUserHasSubmittedClaim } from '../../state/transactions/hooks'
import { Dots } from '../swap/styleds'
import { CardNoise } from '../earn/styled'
import Menu from '../Menu'
import Modal from '../Modal'
import Row, { RowFixed } from '../Row'
import { Dots } from '../swap/styleds'
import Web3Status from '../Web3Status'
import UniBalanceContent from './UniBalanceContent'
const HeaderFrame = styled.div<{ showBackground: boolean }>`
@ -302,16 +298,6 @@ export const StyledMenuButton = styled.button`
}
`
const NETWORK_LABELS: { [chainId in SupportedChainId | number]: string } = {
[SupportedChainId.MAINNET]: 'Mainnet',
[SupportedChainId.RINKEBY]: 'Rinkeby',
[SupportedChainId.ROPSTEN]: 'Ropsten',
[SupportedChainId.GOERLI]: 'Görli',
[SupportedChainId.KOVAN]: 'Kovan',
[SupportedChainId.ARBITRUM_KOVAN]: 'kArbitrum',
[SupportedChainId.ARBITRUM_ONE]: 'Arbitrum One',
}
export default function Header() {
const { account, chainId } = useActiveWeb3React()

@ -16,11 +16,13 @@ export const V2_ROUTER_ADDRESS: AddressMap = constructSameAddressMap(
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
false
)
// most current governance contract address should always be the 0 index
export const GOVERNANCE_ADDRESSES: AddressMap[] = [
constructSameAddressMap('0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F', false),
{
[SupportedChainId.MAINNET]: '0xC4e172459f1E7939D522503B81AFAaC1014CE6F6',
},
constructSameAddressMap('0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F', false),
]
export const TIMELOCK_ADDRESS: AddressMap = constructSameAddressMap('0x1a9C8182C09F50C8318d769245beA52c32BE35BC', false)
export const MERKLE_DISTRIBUTOR_ADDRESS: AddressMap = {

@ -7,3 +7,13 @@ export enum SupportedChainId {
ARBITRUM_KOVAN = 144545313136048,
ARBITRUM_ONE = 42161,
}
export const NETWORK_LABELS: { [chainId in SupportedChainId | number]: string } = {
[SupportedChainId.MAINNET]: 'Mainnet',
[SupportedChainId.RINKEBY]: 'Rinkeby',
[SupportedChainId.ROPSTEN]: 'Ropsten',
[SupportedChainId.GOERLI]: 'Görli',
[SupportedChainId.KOVAN]: 'Kovan',
[SupportedChainId.ARBITRUM_KOVAN]: 'kArbitrum',
[SupportedChainId.ARBITRUM_ONE]: 'Arbitrum One',
}

@ -1,11 +1,21 @@
import { GOVERNANCE_ADDRESSES, TIMELOCK_ADDRESS, UNI_ADDRESS } from './addresses'
import { SupportedChainId } from './chains'
export const COMMON_CONTRACT_NAMES: { [chainId: number]: { [address: string]: string } } = {
[1]: {
[UNI_ADDRESS[1]]: 'UNI',
[GOVERNANCE_ADDRESSES[0][1]]: 'Governance (V0)',
[GOVERNANCE_ADDRESSES[1][1]]: 'Governance',
[TIMELOCK_ADDRESS[1]]: 'Timelock',
// returns { [address]: `Governance (V${n})`} for each address in GOVERNANCE_ADDRESSES except the current, which gets no version indicator
const governanceContracts = (): Record<string, string> =>
GOVERNANCE_ADDRESSES.reduce(
(acc, addressMap, i) => ({
...acc,
[addressMap[SupportedChainId.MAINNET]]: `Governance${i === GOVERNANCE_ADDRESSES.length - 1 ? '' : ` (V${i})`}`,
}),
{}
)
export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }> = {
[SupportedChainId.MAINNET]: {
[UNI_ADDRESS[SupportedChainId.MAINNET]]: 'UNI',
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
...governanceContracts(),
},
}

@ -0,0 +1,2 @@
export const UNISWAP_GRANTS_START_BLOCK = 11473815
export const EDUCATION_FUND_1_START_BLOCK = 12620175

@ -46,19 +46,34 @@ import { useActiveWeb3React } from './web3'
// returns null on errors
export function useContract<T extends Contract = Contract>(
addressOrAddressMap: string | { [chainId: number]: string } | undefined,
addressOrAddressMap: string | { [chainId: number]: string } | { [chainId: number]: string }[] | undefined,
ABI: any,
withSignerIfPossible = true
): T | null {
const { library, account, chainId } = useActiveWeb3React()
return useMemo(() => {
if (!addressOrAddressMap || !ABI || !library || !chainId) return null
if (!addressOrAddressMap || !ABI || !library || !chainId) {
return null
}
let address: string | undefined
if (typeof addressOrAddressMap === 'string') address = addressOrAddressMap
else address = addressOrAddressMap[chainId]
if (!address) return null
if (typeof addressOrAddressMap === 'string') {
address = addressOrAddressMap
} else if (!Array.isArray(addressOrAddressMap)) {
address = addressOrAddressMap[chainId]
}
if (!address && !Array.isArray(addressOrAddressMap)) {
return null
}
try {
if (Array.isArray(addressOrAddressMap)) {
return addressOrAddressMap.map((addressMap) =>
getContract(addressMap[chainId], ABI, library, withSignerIfPossible && account ? account : undefined)
)
}
if (!address) {
return null
}
return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined)
} catch (error) {
console.error('Failed to get contract', error)
@ -116,11 +131,22 @@ export function useMerkleDistributorContract() {
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)
}
export function useGovernanceContracts(): (Contract | null)[] {
return [
useContract(GOVERNANCE_ADDRESSES[0], GOVERNANCE_ABI, false),
useContract(GOVERNANCE_ADDRESSES[1], GOVERNANCE_ABI, true),
]
export function useGovernanceContracts(): Contract[] | null {
const { library, account, chainId } = useActiveWeb3React()
return useMemo(() => {
if (!library || !chainId) {
return null
}
try {
return GOVERNANCE_ADDRESSES.filter((addressMap) => Boolean(addressMap[chainId])).map((addressMap) =>
getContract(addressMap[chainId], GOVERNANCE_ABI, library, account ? account : undefined)
)
} catch (error) {
console.error('Failed to get contract', error)
return null
}
}, [library, chainId, account])
}
export function useUniContract() {

@ -120,8 +120,7 @@ export default function Vote() {
const toggleDelegateModal = useToggleDelegateModal()
// get data to list all proposals
// TODO don't hardcode for first gov alpha
const allProposals: ProposalData[] = useAllProposalData()[0]
const allProposals: ProposalData[] = useAllProposalData()
// user data
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
@ -249,7 +248,7 @@ export default function Vote() {
</TYPE.subHeader>
</EmptyProposals>
)}
{allProposals?.reverse()?.map((p: ProposalData, i) => {
{allProposals?.reverse().map((p: ProposalData, i) => {
return (
<Proposal as={Link} to={'/vote/' + p.id} key={i}>
<ProposalNumber>{p.id}</ProposalNumber>
@ -260,7 +259,7 @@ export default function Vote() {
})}
</TopSection>
<TYPE.subHeader color="text3">
<Trans>A minimum threshold of 1% of the total UNI supply is required to submit proposals</Trans>
<Trans>A minimum threshold of 0.25% of the total UNI supply is required to submit proposals</Trans>
</TYPE.subHeader>
</PageWrapper>
<SwitchLocaleLink />

@ -1,16 +1,18 @@
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { isAddress } from 'ethers/lib/utils'
import { UNI } from '../../constants/tokens'
import { useGovernanceContracts, useUniContract } from '../../hooks/useContract'
import { calculateGasMargin } from '../../utils/calculateGasMargin'
import { useSingleCallResult, useSingleContractMultipleData } from '../multicall/hooks'
import { useActiveWeb3React } from '../../hooks/web3'
import { ethers, utils } from 'ethers'
import { TransactionResponse } from '@ethersproject/providers'
import { useTransactionAdder } from '../transactions/hooks'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { abi as GOV_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { GOVERNANCE_ADDRESSES } from 'constants/addresses'
import { UNISWAP_GRANTS_PROPOSAL_DESCRIPTION } from 'constants/proposals/uniswap_grants_proposal_description'
import { ethers, utils } from 'ethers'
import { isAddress } from 'ethers/lib/utils'
import { useGovernanceContracts, useUniContract } from 'hooks/useContract'
import { useActiveWeb3React } from 'hooks/web3'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { calculateGasMargin } from 'utils/calculateGasMargin'
import { EDUCATION_FUND_1_START_BLOCK, UNISWAP_GRANTS_START_BLOCK } from '../../constants/proposals'
import { UNI } from '../../constants/tokens'
import { useMultipleContractMultipleData, useMultipleContractSingleData, useSingleCallResult } from '../multicall/hooks'
import { useTransactionAdder } from '../transactions/hooks'
interface ProposalDetail {
target: string
@ -43,52 +45,72 @@ export enum ProposalState {
Executed,
}
// get count of all proposals made on the given governor alpha
function useProposalCount(govContract: ethers.Contract | null): number | undefined {
const res = useSingleCallResult(govContract, 'proposalCount')
if (res.result && !res.loading) {
return parseInt(res.result[0])
}
return undefined
const GovernanceInterface = new ethers.utils.Interface(GOV_ABI)
// get count of all proposals made
export function useProposalCounts(): Record<string, number> | undefined {
const { chainId } = useActiveWeb3React()
const addresses = useMemo(() => {
if (!chainId) {
return []
}
return GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId])
}, [chainId])
const responses = useMultipleContractSingleData(addresses, GovernanceInterface, 'proposalCount')
return useMemo(() => {
return responses.reduce((acc, response, i) => {
if (response.result && !response.loading) {
return {
...acc,
[addresses[i]]: parseInt(response.result[0]),
}
}
return acc
}, {})
}, [addresses, responses])
}
/**
* Need proposal events to get description data emitted from
* new proposal event.
*/
const eventParser = new ethers.utils.Interface(GOV_ABI)
function useDataFromEventLogs(govContract: ethers.Contract | null) {
export function useDataFromEventLogs() {
const { library, chainId } = useActiveWeb3React()
const [formattedEvents, setFormattedEvents] =
useState<{ description: string; details: { target: string; functionSig: string; callData: string }[] }[]>()
const govContracts = useGovernanceContracts()
// create filter for these specific events
const filter = useMemo(
const filters = useMemo(
() =>
govContract ? { ...govContract.filters.ProposalCreated(), fromBlock: 10861678, toBlock: 'latest' } : undefined,
[govContract]
govContracts
? govContracts.map((contract) => ({
...contract.filters.ProposalCreated(),
fromBlock: 10861678,
toBlock: 'latest',
}))
: undefined,
[govContracts]
)
useEffect(() => {
if (!filter || !library) return
if (!filters || !library) return
let stale = false
if (!formattedEvents) {
library
.getLogs(filter)
.then((proposalEvents) => {
const filterRequests = filters.map((filter) => library.getLogs(filter))
Promise.all(filterRequests)
.then((events) => events.flat())
.then((governanceContractsProposalEvents) => {
if (stale) return
const formattedEventData = proposalEvents?.map((event) => {
const eventParsed = eventParser.parseLog(event).args
const formattedEventData = governanceContractsProposalEvents.map((event) => {
const eventParsed = GovernanceInterface.parseLog(event).args
return {
description: eventParsed.description,
details: eventParsed.targets.map((target: string, i: number) => {
const signature = eventParsed.signatures[i]
const [name, types] = signature.substr(0, signature.length - 1).split('(')
const calldata = eventParsed.calldatas[i]
const decoded = utils.defaultAbiCoder.decode(types.split(','), calldata)
return {
target,
functionSig: name,
@ -108,99 +130,99 @@ function useDataFromEventLogs(govContract: ethers.Contract | null) {
}
return
}, [filter, library, formattedEvents, chainId])
}, [filters, library, formattedEvents, chainId])
return formattedEvents
}
// get data for all past and active proposals
export function useAllProposalData(): ProposalData[][] {
// fetch all governance contracts
const govContracts = useGovernanceContracts()
export function useAllProposalData() {
const { chainId } = useActiveWeb3React()
const proposalCounts = useProposalCounts()
// fetch the proposal count on the active contract
const proposalCount = useProposalCount(govContracts[govContracts.length - 1])
const proposalIndexes = useMemo(() => {
const results: number[][][] = []
const emptyState = new Array(GOVERNANCE_ADDRESSES.length).fill([], 0)
GOVERNANCE_ADDRESSES.forEach((addressMap, i) => {
results[i] = []
if (!chainId) {
return emptyState
}
const address = addressMap[chainId]
if (!proposalCounts || proposalCounts[address] === undefined) {
return emptyState
}
for (let j = 1; j <= proposalCounts[address]; j++) {
results[i].push([j])
}
return results
})
return results.filter((indexArray) => indexArray.length > 0)
}, [chainId, proposalCounts])
// get all proposals for all contracts
const proposalsIndicesByGovContract = [
[1, 2, 3, 4], // hardcoded for first governor alpha
typeof proposalCount === 'number' ? new Array(proposalCount).fill(0).map((_, i) => i + 1) : [], // dynamic for current governor alpha
]
// get all proposal entities
const allProposalsByGovContract = [
useSingleContractMultipleData(
govContracts[0],
'proposals',
proposalsIndicesByGovContract[0].map((i) => [i])
),
useSingleContractMultipleData(
govContracts[1],
'proposals',
proposalsIndicesByGovContract[1].map((i) => [i])
),
]
// get all proposal states
const allProposalStatesByGovContract = [
useSingleContractMultipleData(
govContracts[0],
'state',
proposalsIndicesByGovContract[0].map((i) => [i])
),
useSingleContractMultipleData(
govContracts[1],
'state',
proposalsIndicesByGovContract[1].map((i) => [i])
),
]
const addresses = useMemo(() => {
if (!chainId) {
return []
}
return GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId]).filter(
(address) => proposalCounts && proposalCounts[address] > 0
)
}, [chainId, proposalCounts])
// get metadata from past events
const formattedEventsByGovContract = [useDataFromEventLogs(govContracts[0]), useDataFromEventLogs(govContracts[1])]
const formattedEvents = useDataFromEventLogs()
const returnData: ProposalData[][] = []
// get all proposal entities
const allProposalsCallData = useMultipleContractMultipleData(
addresses,
GovernanceInterface,
'proposals',
proposalIndexes
).flat()
for (let governorIndex = 0; governorIndex < allProposalsByGovContract.length; governorIndex++) {
const allProposals = allProposalsByGovContract[governorIndex]
const allProposalStates = allProposalStatesByGovContract[governorIndex]
const formattedEvents = formattedEventsByGovContract[governorIndex]
// get all proposal states
const allProposalStatesCallData = useMultipleContractMultipleData(
addresses,
GovernanceInterface,
'state',
proposalIndexes
).flat()
if (
allProposals?.every((p) => Boolean(p.result)) &&
allProposalStates?.every((p) => Boolean(p.result)) &&
formattedEvents?.every((p) => Boolean(p))
) {
returnData.push(
allProposals.map((proposal, i): ProposalData => {
let description = formattedEvents[i].description
// overwrite broken description
if (governorIndex === 0 && i === 2) description = UNISWAP_GRANTS_PROPOSAL_DESCRIPTION
return {
id: proposal?.result?.id.toString(),
title: description?.split(/# |\n/g)[1] ?? 'Untitled',
description: description ?? 'No description.',
proposer: proposal?.result?.proposer,
status: allProposalStates[i]?.result?.[0] ?? ProposalState.Undetermined,
forCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.forVotes.toString(), 18)),
againstCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.againstVotes.toString(), 18)),
startBlock: parseInt(proposal?.result?.startBlock?.toString()),
endBlock: parseInt(proposal?.result?.endBlock?.toString()),
details: formattedEvents[i].details,
}
})
)
} else {
returnData.push([])
}
if (
!allProposalsCallData?.every((p) => Boolean(p.result)) ||
!allProposalStatesCallData?.every((p) => Boolean(p.result)) ||
!formattedEvents?.every((p) => Boolean(p))
) {
return []
}
return returnData
const omittedProposalStartBlocks = [EDUCATION_FUND_1_START_BLOCK]
return allProposalsCallData
.map((proposal, i) => {
let description = formattedEvents[i].description
const startBlock = parseInt(proposal?.result?.startBlock?.toString())
if (startBlock === UNISWAP_GRANTS_START_BLOCK) {
description = UNISWAP_GRANTS_PROPOSAL_DESCRIPTION
}
return {
id: proposal?.result?.id.toString(),
title: description?.split(/# |\n/g)[1] ?? 'Untitled',
description: description ?? 'No description.',
proposer: proposal?.result?.proposer,
status: allProposalStatesCallData[i]?.result?.[0] ?? ProposalState.Undetermined,
forCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.forVotes.toString(), 18)),
againstCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.againstVotes.toString(), 18)),
startBlock,
endBlock: parseInt(proposal?.result?.endBlock?.toString()),
details: formattedEvents[i].details,
}
})
.filter((proposal) => !omittedProposalStartBlocks.includes(proposal.startBlock))
}
export function useProposalData(id: string): ProposalData | undefined {
// TODO don't hardcode for first gov alpha
const allProposalData = useAllProposalData()[0]
const allProposalData = useAllProposalData()
return allProposalData?.find((p) => p.id === id)
}
@ -266,18 +288,16 @@ export function useVoteCallback(): {
} {
const { account } = useActiveWeb3React()
// we only care about voting on the active governance contract
const govContracts = useGovernanceContracts()
const govContract = govContracts[govContracts.length - 1]
const latestGovernanceContract = govContracts ? govContracts[0] : null
const addTransaction = useTransactionAdder()
const voteCallback = useCallback(
(proposalId: string | undefined, support: boolean) => {
if (!account || !govContract || !proposalId) return
if (!account || !latestGovernanceContract || !proposalId) return
const args = [proposalId, support]
return govContract.estimateGas.castVote(...args, {}).then((estimatedGasLimit) => {
return govContract
return latestGovernanceContract.estimateGas.castVote(...args, {}).then((estimatedGasLimit) => {
return latestGovernanceContract
.castVote(...args, { value: null, gasLimit: calculateGasMargin(estimatedGasLimit) })
.then((response: TransactionResponse) => {
addTransaction(response, {
@ -287,7 +307,7 @@ export function useVoteCallback(): {
})
})
},
[account, addTransaction, govContract]
[account, addTransaction, latestGovernanceContract]
)
return { voteCallback }
}

@ -1,4 +1,4 @@
import { Interface, FunctionFragment } from '@ethersproject/abi'
import { FunctionFragment, Interface } from '@ethersproject/abi'
import { BigNumber } from '@ethersproject/bignumber'
import { Contract } from '@ethersproject/contracts'
import { useEffect, useMemo } from 'react'
@ -8,10 +8,10 @@ import { useBlockNumber } from '../application/hooks'
import {
addMulticallListeners,
Call,
removeMulticallListeners,
parseCallKey,
toCallKey,
ListenerOptions,
parseCallKey,
removeMulticallListeners,
toCallKey,
} from './actions'
export interface Result extends ReadonlyArray<any> {
@ -231,6 +231,63 @@ export function useMultipleContractSingleData(
}, [fragment, results, contractInterface, latestBlockNumber])
}
export function useMultipleContractMultipleData(
addresses: (string | undefined)[],
contractInterface: Interface,
methodName: string,
callInputs?: OptionalMethodInputs[][],
options?: ListenerOptions,
gasRequired?: number
): CallState[][] {
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
const calls = useMemo(() => {
return addresses.map((address, i) => {
const passesChecks = address && contractInterface && fragment
if (!passesChecks) {
return []
}
return callInputs
? callInputs[i].map<Call | undefined>((inputs) => {
const callData: string | undefined = isValidMethodArgs(inputs)
? contractInterface.encodeFunctionData(fragment, inputs)
: undefined
return address && callData
? {
address,
callData,
...(gasRequired ? { gasRequired } : {}),
}
: undefined
})
: []
})
}, [addresses, callInputs, contractInterface, fragment, gasRequired])
const callResults = useCallsData(calls.flat(), options)
const latestBlockNumber = useBlockNumber()
return useMemo(() => {
const callInputLengths = callInputs ? callInputs.map((inputArray) => inputArray.length) : []
const unformatedResults = callResults.map((result) =>
toCallState(result, contractInterface, fragment, latestBlockNumber)
)
return callInputLengths.map((length) => {
let j = 0
const indexElements: any = []
while (j < length) {
indexElements.push(unformatedResults.shift())
j++
}
return indexElements
})
}, [fragment, callInputs, callResults, contractInterface, latestBlockNumber])
}
export function useSingleCallResult(
contract: Contract | null | undefined,
methodName: string,