diff --git a/src/hooks/useIndexDTF.ts b/src/hooks/useIndexDTF.ts index 91a347bcf..c6d78c6b8 100644 --- a/src/hooks/useIndexDTF.ts +++ b/src/hooks/useIndexDTF.ts @@ -33,6 +33,7 @@ type DTFQueryResponse = { executionDelay: number } } + legacyAdmins: Address[] tradingGovernance?: { id: Address votingDelay: number @@ -45,6 +46,7 @@ type DTFQueryResponse = { executionDelay: number } } + legacyAuctionApprovers: Address[] token: { id: Address name: string @@ -78,6 +80,7 @@ type DTFQueryResponse = { executionDelay: number } } + legacyGovernance: Address[] rewards: { rewardToken: { address: Address @@ -128,6 +131,7 @@ const dtfQuery = gql` executionDelay } } + legacyAdmins tradingGovernance { id votingDelay @@ -140,6 +144,7 @@ const dtfQuery = gql` executionDelay } } + legacyAuctionApprovers token { id name @@ -173,6 +178,7 @@ const dtfQuery = gql` executionDelay } } + legacyGovernance rewards(where: { active: true }) { rewardToken { address diff --git a/src/types/index.ts b/src/types/index.ts index ceb0e38ee..9b169c139 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -292,6 +292,7 @@ export type IndexDTF = { executionDelay: number } } + legacyAdmins: Address[] tradingGovernance?: { id: Address votingDelay: number @@ -304,6 +305,7 @@ export type IndexDTF = { executionDelay: number } } + legacyAuctionApprovers: Address[] token: { id: Address name: string @@ -337,6 +339,7 @@ export type IndexDTF = { executionDelay: number } } + legacyGovernance: Address[] rewardTokens: Token[] } totalRevenue: number diff --git a/src/views/index-dtf/deploy/steps/confirm-deploy/manual/components/confirm-manual-deploy-button.tsx b/src/views/index-dtf/deploy/steps/confirm-deploy/manual/components/confirm-manual-deploy-button.tsx index 57e0212e4..775a06854 100644 --- a/src/views/index-dtf/deploy/steps/confirm-deploy/manual/components/confirm-manual-deploy-button.tsx +++ b/src/views/index-dtf/deploy/steps/confirm-deploy/manual/components/confirm-manual-deploy-button.tsx @@ -174,9 +174,8 @@ const txAtom = atom< const ownerGovernanceConfig: GovernanceConfig = { votingDelay: Math.floor((formData.governanceVotingDelay || 0) * 86400), votingPeriod: Math.floor((formData.governanceVotingPeriod || 0) * 86400), - proposalThreshold: parseEther( - (formData.governanceVotingThreshold || 0).toString() - ), + proposalThreshold: + parseEther((formData.governanceVotingThreshold || 0).toString()) / 100n, quorumPercent: BigInt(Math.floor(formData.governanceVotingQuorum || 0)), timelockDelay: BigInt( Math.floor((formData.governanceExecutionDelay || 0) * 86400) @@ -187,9 +186,8 @@ const txAtom = atom< const tradingGovernanceConfig: GovernanceConfig = { votingDelay: Math.floor((formData.basketVotingDelay || 0) * 3600), votingPeriod: Math.floor((formData.basketVotingPeriod || 0) * 3600), - proposalThreshold: parseEther( - (formData.basketVotingThreshold || 0).toString() - ), + proposalThreshold: + parseEther((formData.basketVotingThreshold || 0).toString()) / 100n, quorumPercent: BigInt(Math.floor(formData.basketVotingQuorum || 0)), timelockDelay: BigInt( Math.floor((formData.basketExecutionDelay || 0) * 3600) diff --git a/src/views/index-dtf/governance/components/governance-proposal-preview.tsx b/src/views/index-dtf/governance/components/governance-proposal-preview.tsx index 42ccad15f..608b0475e 100644 --- a/src/views/index-dtf/governance/components/governance-proposal-preview.tsx +++ b/src/views/index-dtf/governance/components/governance-proposal-preview.tsx @@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai' import { Address, decodeFunctionData, getAbiItem } from 'viem' import dtfIndexAbi from '@/abis/dtf-index-abi' +import dtfAdminAbi from '@/abis/dtf-admin-abi' import dtfIndexGovernance from '@/abis/dtf-index-governance' import dtfIndexStakingVault from '@/abis/dtf-index-staking-vault' import { Button } from '@/components/ui/button' @@ -26,6 +27,10 @@ import { Abi, Hex } from 'viem' import BasketProposalPreview from '../views/propose/basket/components/proposal-basket-preview' import RawCallPreview from './proposal-preview/raw-call-preview' import TokenRewardPreview from './proposal-preview/token-reward-preview' +import { + spellAbi as governanceSpell_31_03_2025Abi, + spellAddress as governanceSpell_31_03_2025Address, +} from '../views/propose/components/propose-governance-spell-31-03-2025' const dtfAbiMapppingAtom = atom((get) => { const dtf = get(indexDTFAtom) @@ -34,6 +39,7 @@ const dtfAbiMapppingAtom = atom((get) => { const abiMapping: Record = { [dtf.id.toLowerCase()]: dtfIndexAbi, + [dtf.proxyAdmin.toLowerCase()]: dtfAdminAbi, } if (dtf.ownerGovernance) { @@ -52,6 +58,11 @@ const dtfAbiMapppingAtom = atom((get) => { } } + if (governanceSpell_31_03_2025Address[dtf.chainId]) { + abiMapping[governanceSpell_31_03_2025Address[dtf.chainId].toLowerCase()] = + governanceSpell_31_03_2025Abi + } + return abiMapping }) @@ -62,6 +73,7 @@ const dtfContractAliasAtom = atom((get) => { const aliasMapping: Record = { [dtf.id.toLowerCase()]: 'Folio', + [dtf.proxyAdmin.toLowerCase()]: 'ProxyAdmin', } if (dtf.ownerGovernance) { @@ -80,6 +92,11 @@ const dtfContractAliasAtom = atom((get) => { } } + if (governanceSpell_31_03_2025Address[dtf.chainId]) { + aliasMapping[governanceSpell_31_03_2025Address[dtf.chainId].toLowerCase()] = + 'GovernanceSpell_31_03_2025' + } + return aliasMapping }) diff --git a/src/views/index-dtf/governance/updater.tsx b/src/views/index-dtf/governance/updater.tsx index 50c4d674a..a11b2b9b7 100644 --- a/src/views/index-dtf/governance/updater.tsx +++ b/src/views/index-dtf/governance/updater.tsx @@ -9,6 +9,10 @@ import { Address, formatEther } from 'viem' import { indexGovernanceOverviewAtom, refetchTokenAtom } from './atoms' type Response = { + governances: { + proposals: PartialProposal[] + proposalCount: number + }[] ownerGovernance: { proposals: PartialProposal[] proposalCount: number @@ -36,57 +40,9 @@ type Response = { } const query = gql` - query getGovernanceStats( - $ownerGovernance: String! - $tradingGovernance: String! - $vaultGovernance: String! - $stToken: String! - ) { - ownerGovernance: governance(id: $ownerGovernance) { - proposals { - id - description - creationTime - state - forWeightedVotes - abstainWeightedVotes - againstWeightedVotes - executionETA - quorumVotes - voteStart - voteEnd - executionBlock - executionTime - creationBlock - proposer { - address - } - } - proposalCount - } - tradingGovernance: governance(id: $tradingGovernance) { - proposals { - id - description - creationTime - state - forWeightedVotes - abstainWeightedVotes - againstWeightedVotes - executionETA - executionTime - quorumVotes - voteStart - voteEnd - executionBlock - creationBlock - proposer { - address - } - } - proposalCount - } - vaultGovernance: governance(id: $vaultGovernance) { + query getGovernanceStats($governanceIds: [String!]!, $stToken: String!) { + governances(where: { id_in: $governanceIds }) { + id proposals { id description @@ -109,6 +65,7 @@ const query = gql` proposalCount } stakingToken(id: $stToken) { + id totalDelegates token { totalSupply @@ -142,23 +99,26 @@ const Updater = () => { INDEX_DTF_SUBGRAPH_URL[chainId], query, { - ownerGovernance: dtf?.ownerGovernance?.id ?? '', - tradingGovernance: dtf?.tradingGovernance?.id ?? '', - vaultGovernance: dtf?.stToken?.governance?.id ?? '', + governanceIds: [ + dtf?.ownerGovernance?.id, + ...(dtf?.legacyAdmins || []), + dtf?.tradingGovernance?.id, + ...(dtf?.legacyAuctionApprovers || []), + dtf?.stToken?.governance?.id, + ...(dtf?.stToken?.legacyGovernance || []), + ], stToken: dtf?.stToken?.id ?? '', } ) return { - proposals: [ - ...(data.ownerGovernance.proposals ?? []), - ...(data.tradingGovernance?.proposals ?? []), - ...(data.vaultGovernance?.proposals ?? []), - ].sort((a, b) => b.creationTime - a.creationTime), - proposalCount: - +data.ownerGovernance.proposalCount + - +(data.tradingGovernance?.proposalCount ?? 0) + - +(data.vaultGovernance?.proposalCount ?? 0), + proposals: data.governances + .flatMap((g) => g.proposals) + .sort((a, b) => b.creationTime - a.creationTime), + proposalCount: data.governances.reduce( + (x, y) => x + Number(y.proposalCount), + 0 + ), delegates: data.stakingToken?.delegates ?? [], delegatesCount: +(data.stakingToken?.totalDelegates ?? 0), voteSupply: +formatEther(data.stakingToken?.token.totalSupply ?? 0n), diff --git a/src/views/index-dtf/governance/views/propose/components/proposal-type-selection.tsx b/src/views/index-dtf/governance/views/propose/components/proposal-type-selection.tsx index b0333cc20..c309fa94d 100644 --- a/src/views/index-dtf/governance/views/propose/components/proposal-type-selection.tsx +++ b/src/views/index-dtf/governance/views/propose/components/proposal-type-selection.tsx @@ -11,6 +11,7 @@ import { } from 'lucide-react' import { Link } from 'react-router-dom' import ProposeIndexUpgrade from './propose-index-upgrade' +import ProposeGovernanceSpell31032025 from './propose-governance-spell-31-03-2025' const proposalTypes = [ { @@ -92,7 +93,7 @@ const ProposalTypeSelection = () => {
- +
diff --git a/src/views/index-dtf/governance/views/propose/components/propose-governance-spell-31-03-2025.tsx b/src/views/index-dtf/governance/views/propose/components/propose-governance-spell-31-03-2025.tsx new file mode 100644 index 000000000..d1d78b7e2 --- /dev/null +++ b/src/views/index-dtf/governance/views/propose/components/propose-governance-spell-31-03-2025.tsx @@ -0,0 +1,346 @@ +import dtfIndexAbi from '@/abis/dtf-index-abi' +import dtfAdminAbi from '@/abis/dtf-admin-abi' +import stakingVaultAbi from '@/abis/dtf-index-staking-vault' +import DTFIndexGovernance from '@/abis/dtf-index-governance' +import { Button } from '@/components/ui/button' +import { chainIdAtom } from '@/state/atoms' +import { indexDTFAtom } from '@/state/dtf/atoms' +import { PROPOSAL_STATES } from '@/utils/constants' +import { useAtomValue, useSetAtom } from 'jotai' +import { AlertCircle, Loader2 } from 'lucide-react' +import { useCallback, useEffect } from 'react' +import { + encodeFunctionData, + getAddress, + isAddressEqual, + keccak256, + pad, + parseAbi, + toBytes, +} from 'viem' +import { + useReadContract, + useWaitForTransactionReceipt, + useWriteContract, +} from 'wagmi' +import { useIsProposeAllowed } from '../../../hooks/use-is-propose-allowed' +import { ChainId } from '@/utils/chains' +import { getCurrentTime } from '@/utils' +import { governanceProposalsAtom, refetchTokenAtom } from '../../../atoms' +import { getProposalState, PartialProposal } from '@/lib/governance' + +export const spellAbi = parseAbi([ + 'function upgradeStakingVaultGovernance(address stakingVault, address oldGovernor, address[] calldata guardians, bytes32 deploymentNonce) external returns (address newGovernor)', + 'function upgradeFolioGovernance(address folio, address proxyAdmin, address oldOwnerGovernor, address oldTradingGovernor, address[] calldata ownerGuardians, address[] calldata tradingGuardians, bytes32 deploymentNonce) external returns (address newOwnerGovernor, address newTradingGovernor)', +]) + +export const spellAddress = { + [ChainId.Mainnet]: getAddress('0x880F6ef00d13bAf60f3B99099451432F502EdA15'), + [ChainId.Base]: getAddress('0xE7FAa62c3F71f743F3a2Fc442393182F6B64f156'), +} + +const UPGRADE_FOLIO_MESSAGE = 'Upgrade Folio Governance' +const UPGRADE_FOLIO_DAO_MESSAGE = 'Upgrade Folio DAO Governor' + +const queryParams = { + staleTime: 5 * 60 * 1000, + refetchInterval: 5 * 60 * 1000, +} as const + +type SpellUpgradeProps = { + refetch: () => void +} + +const ProposeGovernanceSpell31032025Folio = ({ + refetch, +}: SpellUpgradeProps) => { + const dtf = useAtomValue(indexDTFAtom) + const chainId = useAtomValue(chainIdAtom) + const spell = spellAddress[chainId] + + const { data: qdOwnerGov } = useReadContract({ + abi: DTFIndexGovernance, + address: dtf?.ownerGovernance?.id, + functionName: 'quorumDenominator', + chainId, + query: queryParams, + }) + + const { writeContract, data, isPending } = useWriteContract() + + const { isSuccess } = useWaitForTransactionReceipt({ + hash: data, + chainId, + }) + + const isReady = + dtf?.id && + dtf?.proxyAdmin && + dtf?.ownerGovernance?.id && + dtf?.tradingGovernance?.id + const proposalAvailable = !!spell && qdOwnerGov === 100n + + const handlePropose = () => { + if (!dtf || !spell) return + if (!dtf.ownerGovernance || !dtf.tradingGovernance || qdOwnerGov !== 100n) + return + + const oldOwnerGovernor = dtf.ownerGovernance.id + const oldTradingGovernor = dtf.tradingGovernance.id + + const ownerGuardians = dtf.ownerGovernance.timelock.guardians.filter( + (guardian) => !isAddressEqual(guardian, oldOwnerGovernor) + ) + const tradingGuardians = dtf.tradingGovernance.timelock.guardians.filter( + (guardian) => !isAddressEqual(guardian, oldTradingGovernor) + ) + + writeContract({ + address: dtf.ownerGovernance.id, + abi: DTFIndexGovernance, + functionName: 'propose', + args: [ + [dtf.id, dtf.proxyAdmin, spell], + [0n, 0n, 0n], + [ + encodeFunctionData({ + abi: dtfIndexAbi, + functionName: 'grantRole', + args: [pad('0x0', { size: 32 }), spell], + }), + encodeFunctionData({ + abi: dtfAdminAbi, + functionName: 'transferOwnership', + args: [spell], + }), + encodeFunctionData({ + abi: spellAbi, + functionName: 'upgradeFolioGovernance', + args: [ + dtf.id, + dtf.proxyAdmin, + oldOwnerGovernor, + oldTradingGovernor, + ownerGuardians, + tradingGuardians, + keccak256(toBytes(getCurrentTime())), + ], + }), + ], + UPGRADE_FOLIO_MESSAGE, + ], + }) + } + + useEffect(() => { + if (isSuccess) { + // Give some time for the proposal to be created on the subgraph + setTimeout(() => { + refetch() + }, 10000) + } + }, [isSuccess]) + + if (!proposalAvailable) { + return null + } + + return ( +
+
+ +
+

+ Upgrade Folio Governance (1/2): +

+

+ This upgrade spell fixes the proposal-threshold on the Admin and + Basket FolioGovernor contracts that manage this DTF. Once this spell + is approved and executed, the proposal-threshold will be the correct + % that was initially intended at deployment. +

+
+
+ +
+ ) +} + +const ProposeGovernanceSpell31032025StakingVault = ({ + refetch, +}: SpellUpgradeProps) => { + const dtf = useAtomValue(indexDTFAtom) + const chainId = useAtomValue(chainIdAtom) + const spell = spellAddress[chainId] + + const { data: qdStakingVaultGov } = useReadContract({ + abi: DTFIndexGovernance, + address: dtf?.stToken?.governance?.id, + functionName: 'quorumDenominator', + chainId, + query: queryParams, + }) + + const { writeContract, data, isPending } = useWriteContract() + + const { isSuccess } = useWaitForTransactionReceipt({ + hash: data, + chainId, + }) + + const isReady = dtf?.id && dtf?.proxyAdmin && dtf?.stToken?.governance?.id + const proposalAvailable = !!spell && qdStakingVaultGov === 100n + + const handlePropose = () => { + if (!dtf || !spell) return + if (!dtf?.stToken?.governance || qdStakingVaultGov !== 100n) return + + const oldGovernor = dtf.stToken.governance.id + const guardians = dtf.stToken.governance.timelock.guardians.filter( + (guardian) => !isAddressEqual(guardian, oldGovernor) + ) + + writeContract({ + address: dtf.stToken.governance.id, + abi: DTFIndexGovernance, + functionName: 'propose', + args: [ + [dtf.stToken.id, spell], + [0n, 0n], + [ + encodeFunctionData({ + abi: stakingVaultAbi, + functionName: 'transferOwnership', + args: [spell], + }), + encodeFunctionData({ + abi: spellAbi, + functionName: 'upgradeStakingVaultGovernance', + args: [ + dtf.stToken.id, + oldGovernor, + guardians, + keccak256(toBytes(getCurrentTime())), + ], + }), + ], + UPGRADE_FOLIO_DAO_MESSAGE, + ], + }) + } + + useEffect(() => { + if (isSuccess) { + // Give some time for the proposal to be created on the subgraph + setTimeout(() => { + refetch() + }, 10000) + } + }, [isSuccess]) + + if (!proposalAvailable) { + return null + } + + return ( +
+
+ +
+

+ Upgrade Folio DAO Governor (2/2): +

+

+ This upgrade spell fixes the proposal-threshold on the FolioGovernor + contract that administers the DAO (StakingVault) relevant to this + DTF. Once this spell is approved and executed, the + proposal-threshold will be the correct % that was initially intended + at deployment. +

+
+
+ +
+ ) +} + +const validProposalExists = ( + proposals: PartialProposal[], + description: string +): boolean => { + const states = [ + PROPOSAL_STATES.PENDING, + PROPOSAL_STATES.ACTIVE, + PROPOSAL_STATES.SUCCEEDED, + PROPOSAL_STATES.QUEUED, + PROPOSAL_STATES.EXECUTED, + ] + return proposals.some((p) => { + if (p.description !== description) { + return false + } + + const pState = getProposalState(p) + + if (pState.state === PROPOSAL_STATES.EXPIRED) { + return false + } + + return states.includes(pState.state) + }) +} + +// TODO(jg): Enable Staking Vault spell when DAO gov is fixed +export default function ProposeGovernanceSpell31032025() { + const { isProposeAllowed } = useIsProposeAllowed() + const proposals = useAtomValue(governanceProposalsAtom) + const setRefetchToken = useSetAtom(refetchTokenAtom) + + const refetch = useCallback(() => { + setRefetchToken(getCurrentTime()) + }, [setRefetchToken]) + + if (!isProposeAllowed || !proposals) return null + + const existsFolioUpgrade = validProposalExists( + proposals, + UPGRADE_FOLIO_MESSAGE + ) + const existsFolioDaoUpgrade = validProposalExists( + proposals, + UPGRADE_FOLIO_DAO_MESSAGE + ) + + return ( + <> + {!existsFolioUpgrade && ( + + )} + {!existsFolioDaoUpgrade && false && ( + + )} + + ) +} diff --git a/src/views/portfolio/sidebar/components/actions.tsx b/src/views/portfolio/sidebar/components/actions.tsx index 83dfb31c6..c6c10edee 100644 --- a/src/views/portfolio/sidebar/components/actions.tsx +++ b/src/views/portfolio/sidebar/components/actions.tsx @@ -265,6 +265,7 @@ export const ModifyLockAction = ({ stToken }: { stToken: StakingToken }) => { address: stToken.underlying.address, decimals: stToken.underlying.decimals, }, + legacyGovernance: [], chainId: stToken.chainId, }