From b9e7329fc951cba4e9a9794abe4f42cc5c1c5f23 Mon Sep 17 00:00:00 2001 From: ChefJoJo <94336009+chef-jojo@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:25:11 +0800 Subject: [PATCH] feat: VeCAKE voting power (#8474) --- apps/web/src/utils/calls/pools.ts | 4 +- .../components/CastVoteModal/DetailsView.tsx | 17 ++- .../components/CastVoteModal/MainView.tsx | 122 ++++++++++++++++-- .../Voting/components/CastVoteModal/index.tsx | 57 +++++--- .../Voting/components/VoteDetailsModal.tsx | 35 +++-- apps/web/src/views/Voting/helpers.ts | 30 ++++- .../views/Voting/hooks/useGetVotingPower.tsx | 15 ++- apps/web/src/views/Voting/strategies.ts | 31 ++++- .../localization/src/config/translations.json | 2 + 9 files changed, 251 insertions(+), 62 deletions(-) diff --git a/apps/web/src/utils/calls/pools.ts b/apps/web/src/utils/calls/pools.ts index d55854c92608e..794ae1ea3c5e4 100644 --- a/apps/web/src/utils/calls/pools.ts +++ b/apps/web/src/utils/calls/pools.ts @@ -1,5 +1,5 @@ import { ChainId } from '@pancakeswap/chains' -import { getPoolsConfig } from '@pancakeswap/pools' +import { SerializedPool, getPoolsConfig } from '@pancakeswap/pools' import chunk from 'lodash/chunk' import { publicClient } from 'utils/wagmi' @@ -36,7 +36,7 @@ const ABI = [ /** * Returns the total number of pools that were active at a given block */ -export const getActivePools = async (chainId: ChainId, block?: number) => { +export const getActivePools = async (chainId: ChainId, block?: number): Promise => { const poolsConfig = getPoolsConfig(chainId) const eligiblePools = poolsConfig .filter((pool) => pool.sousId !== 0) diff --git a/apps/web/src/views/Voting/components/CastVoteModal/DetailsView.tsx b/apps/web/src/views/Voting/components/CastVoteModal/DetailsView.tsx index 553cdace745c5..02ceeeeaa4bb7 100644 --- a/apps/web/src/views/Voting/components/CastVoteModal/DetailsView.tsx +++ b/apps/web/src/views/Voting/components/CastVoteModal/DetailsView.tsx @@ -1,14 +1,15 @@ -import { useMemo } from 'react' -import BigNumber from 'bignumber.js' -import { Flex, Text, Box, HelpIcon, useTooltip, RocketIcon, ScanLink, Link } from '@pancakeswap/uikit' import { useTranslation } from '@pancakeswap/localization' -import { styled } from 'styled-components' -import { getBlockExploreLink } from 'utils' +import { Box, Flex, HelpIcon, Link, RocketIcon, ScanLink, Text, useTooltip } from '@pancakeswap/uikit' import { formatNumber } from '@pancakeswap/utils/formatBalance' +import BigNumber from 'bignumber.js' +import { useActiveChainId } from 'hooks/useActiveChainId' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' +import { useMemo } from 'react' +import { styled } from 'styled-components' +import { getBlockExploreLink } from 'utils' import { ModalInner, VotingBoxBorder, VotingBoxCardInner } from './styles' -const StyledScanLink = styled(ScanLink)` +export const StyledScanLink = styled(ScanLink)` display: inline-flex; font-size: 14px; > svg { @@ -73,6 +74,8 @@ const DetailsView: React.FC> = ({ const { t } = useTranslation() const blockTimestamp = useCurrentBlockTimestamp() + const { chainId } = useActiveChainId() + const isBoostingExpired = useMemo(() => { return lockedEndTime !== 0 && new BigNumber(blockTimestamp?.toString()).gte(lockedEndTime) }, [blockTimestamp, lockedEndTime]) @@ -124,7 +127,7 @@ const DetailsView: React.FC> = ({ {t('Your voting power at block')} - + {block} diff --git a/apps/web/src/views/Voting/components/CastVoteModal/MainView.tsx b/apps/web/src/views/Voting/components/CastVoteModal/MainView.tsx index 792eb2427ad0e..9547a0846eba2 100644 --- a/apps/web/src/views/Voting/components/CastVoteModal/MainView.tsx +++ b/apps/web/src/views/Voting/components/CastVoteModal/MainView.tsx @@ -1,21 +1,25 @@ -import { useMemo } from 'react' -import BigNumber from 'bignumber.js' +import { useTranslation } from '@pancakeswap/localization' import { - IconButton, - Text, - Skeleton, - Button, AutoRenewIcon, + Button, ChevronRightIcon, - Message, Flex, + IconButton, + Message, RocketIcon, + Skeleton, + Text, } from '@pancakeswap/uikit' -import { useTranslation } from '@pancakeswap/localization' import { formatNumber } from '@pancakeswap/utils/formatBalance' +import BigNumber from 'bignumber.js' +import { useActiveChainId } from 'hooks/useActiveChainId' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' +import { useMemo } from 'react' +import { getBlockExploreLink } from 'utils' +import { MyVeCakeCard } from 'views/CakeStaking/components/MyVeCakeCard' import TextEllipsis from '../TextEllipsis' -import { VotingBoxBorder, VotingBoxCardInner, ModalInner } from './styles' +import { StyledScanLink } from './DetailsView' +import { ModalInner, VotingBoxBorder, VotingBoxCardInner } from './styles' import { CastVoteModalProps } from './types' interface MainViewProps { @@ -35,6 +39,104 @@ interface MainViewProps { onDismiss: CastVoteModalProps['onDismiss'] } +type VeMainViewProps = { + vote?: { + label: string + value: number + } + isLoading?: boolean + isPending?: boolean + isError?: boolean + total: number + disabled?: boolean + veCakeBalance?: number + onConfirm?: () => void + onDismiss?: CastVoteModalProps['onDismiss'] + block: number +} + +export const VeMainView = ({ + vote, + total, + isPending, + isLoading, + isError, + onConfirm, + onDismiss, + disabled, + block, + veCakeBalance, +}: VeMainViewProps) => { + const { t } = useTranslation() + + const { chainId } = useActiveChainId() + + return ( + <> + + {vote ? ( + <> + + {t('Voting For')} + + + {vote.label} + + + ) : null} + + + {t('Your voting power at block')} + + {block} + + + {isLoading && !isError ? ( + + ) : isError ? ( + + {t('Error occurred, please try again later')} + + ) : ( + <> +
+ +
+ + {t( + 'Your voting power is determined by the number of veCAKE you have at the block detailed above. CAKE held in other places does NOT contribute to your voting power.', + )} + +
+ {onConfirm && ( + + {t('Once confirmed, voting action cannot be undone.')} + + )} + + )} +
+ {onConfirm && ( + + )} + {onDismiss && ( + + )} + + ) +} + const MainView: React.FC> = ({ vote, total, @@ -54,7 +156,7 @@ const MainView: React.FC> = ({ const hasLockedCake = lockedCakeBalance > 0 const isBoostingExpired = useMemo(() => { - return lockedEndTime !== 0 && new BigNumber(blockTimestamp?.toString()).gte(lockedEndTime) + return lockedEndTime !== 0 && new BigNumber(blockTimestamp?.toString() ?? 0).gte(lockedEndTime) }, [blockTimestamp, lockedEndTime]) const hasBoosted = hasLockedCake && !isBoostingExpired diff --git a/apps/web/src/views/Voting/components/CastVoteModal/index.tsx b/apps/web/src/views/Voting/components/CastVoteModal/index.tsx index 3b92dfdbe886a..bcad816766744 100644 --- a/apps/web/src/views/Voting/components/CastVoteModal/index.tsx +++ b/apps/web/src/views/Voting/components/CastVoteModal/index.tsx @@ -1,13 +1,14 @@ import { useTranslation } from '@pancakeswap/localization' import { Box, Modal, useToast } from '@pancakeswap/uikit' -import { useAccount, useWalletClient } from 'wagmi' import snapshot from '@snapshot-labs/snapshot.js' import useTheme from 'hooks/useTheme' import { useState } from 'react' import { PANCAKE_SPACE } from 'views/Voting/config' +import { VECAKE_VOTING_POWER_BLOCK } from 'views/Voting/helpers' +import { useAccount, useWalletClient } from 'wagmi' import useGetVotingPower from '../../hooks/useGetVotingPower' import DetailsView from './DetailsView' -import MainView from './MainView' +import MainView, { VeMainView } from './MainView' import { CastVoteModalProps, ConfirmVoteView } from './types' const hub = 'https://hub.snapshot.org' @@ -39,10 +40,11 @@ const CastVoteModal: React.FC> = ({ ifoPoolBalance, lockedCakeBalance, lockedEndTime, + veCakeBalance, } = useGetVotingPower(block) const isStartView = view === ConfirmVoteView.MAIN - const handleBack = isStartView ? null : () => setView(ConfirmVoteView.MAIN) + const handleBack = isStartView ? undefined : () => setView(ConfirmVoteView.MAIN) const handleViewDetails = () => setView(ConfirmVoteView.DETAILS) const title = { @@ -51,7 +53,7 @@ const CastVoteModal: React.FC> = ({ } const handleDismiss = () => { - onDismiss() + onDismiss?.() } const handleConfirmVote = async () => { @@ -61,7 +63,7 @@ const CastVoteModal: React.FC> = ({ getSigner: () => { return { _signTypedData: (domain, types, message) => - signer.signTypedData({ + signer?.signTypedData({ account, domain, types, @@ -72,6 +74,10 @@ const CastVoteModal: React.FC> = ({ }, } + if (!account) { + return + } + await client.vote(web3 as any, account, { space: PANCAKE_SPACE, choice: vote.value, @@ -101,20 +107,33 @@ const CastVoteModal: React.FC> = ({ headerBackground={theme.colors.gradientCardHeader} > - {view === ConfirmVoteView.MAIN && ( - - )} + {view === ConfirmVoteView.MAIN && + (!block || BigInt(block) >= VECAKE_VOTING_POWER_BLOCK ? ( + + ) : ( + + ))} {view === ConfirmVoteView.DETAILS && ( > ifoPoolBalance, lockedCakeBalance, lockedEndTime, + veCakeBalance, } = useGetVotingPower(block) const { theme } = useTheme() const handleDismiss = () => { - onDismiss() + onDismiss?.() } return ( @@ -37,18 +40,22 @@ const VoteDetailsModal: React.FC> ) : ( <> - + {!block || BigInt(block) >= VECAKE_VOTING_POWER_BLOCK ? ( + + ) : ( + + )} diff --git a/apps/web/src/views/Voting/helpers.ts b/apps/web/src/views/Voting/helpers.ts index fab3611f45aa7..ed7432c82349a 100644 --- a/apps/web/src/views/Voting/helpers.ts +++ b/apps/web/src/views/Voting/helpers.ts @@ -1,15 +1,15 @@ -import { createPublicClient, http } from 'viem' +import { cakeVaultV2ABI } from '@pancakeswap/pools' import { bscTokens } from '@pancakeswap/tokens' import BigNumber from 'bignumber.js' import { SNAPSHOT_HUB_API } from 'config/constants/endpoints' import fromPairs from 'lodash/fromPairs' import groupBy from 'lodash/groupBy' import { Proposal, ProposalState, ProposalType, Vote } from 'state/types' -import { bsc } from 'viem/chains' -import { cakeVaultV2ABI } from '@pancakeswap/pools' -import { Address } from 'wagmi' import { getCakeVaultAddress } from 'utils/addressHelpers' +import { createPublicClient, http } from 'viem' +import { bsc } from 'viem/chains' import { convertSharesToCake } from 'views/Pools/helpers' +import { Address } from 'wagmi' import { ADMINS, PANCAKE_SPACE, SNAPSHOT_VERSION } from './config' import { getScores } from './getScores' import * as strategies from './strategies' @@ -98,10 +98,12 @@ export const VOTING_POWER_BLOCK = { v1: 17137653n, } +export const VECAKE_VOTING_POWER_BLOCK = 34371669n + /** * Get voting power by single user for each category */ -interface GetVotingPowerType { +type GetVotingPowerType = { total: number voter: string poolsBalance?: number @@ -114,11 +116,29 @@ interface GetVotingPowerType { lockedEndTime?: number } +// Voting power for veCake holders +type GetVeVotingPowerType = { + total: number + voter: string + veCakeBalance: number +} + const nodeRealProvider = createPublicClient({ transport: http(`https://bsc-mainnet.nodereal.io/v1/${process.env.NEXT_PUBLIC_NODE_REAL_API_ETH}`), chain: bsc, }) +export const getVeVotingPower = async (account: Address, blockNumber?: bigint): Promise => { + const scores = await getScores(PANCAKE_SPACE, STRATEGIES, NETWORK, [account], Number(blockNumber)) + const result = scores[0][account] + + return { + total: result, + voter: account, + veCakeBalance: result, + } +} + export const getVotingPower = async ( account: Address, poolAddresses: Address[], diff --git a/apps/web/src/views/Voting/hooks/useGetVotingPower.tsx b/apps/web/src/views/Voting/hooks/useGetVotingPower.tsx index 093636773ecd6..dcd6929d45f83 100644 --- a/apps/web/src/views/Voting/hooks/useGetVotingPower.tsx +++ b/apps/web/src/views/Voting/hooks/useGetVotingPower.tsx @@ -1,10 +1,10 @@ import { ChainId } from '@pancakeswap/chains' -import { useAccount, Address } from 'wagmi' +import { bscTokens } from '@pancakeswap/tokens' import { useQuery } from '@tanstack/react-query' import { getActivePools } from 'utils/calls' -import { bscTokens } from '@pancakeswap/tokens' import { publicClient } from 'utils/wagmi' -import { getVotingPower } from '../helpers' +import { Address, useAccount } from 'wagmi' +import { VECAKE_VOTING_POWER_BLOCK, getVeVotingPower, getVotingPower } from '../helpers' interface State { cakeBalance?: number @@ -16,6 +16,7 @@ interface State { total: number lockedCakeBalance?: number lockedEndTime?: number + veCakeBalance?: number } const useGetVotingPower = (block?: number): State & { isLoading: boolean; isError: boolean } => { @@ -23,7 +24,13 @@ const useGetVotingPower = (block?: number): State & { isLoading: boolean; isErro const { data, status, error } = useQuery( [account, block, 'votingPower'], async () => { + if (!account) { + throw new Error('No account') + } const blockNumber = block ? BigInt(block) : await publicClient({ chainId: ChainId.BSC }).getBlockNumber() + if (blockNumber >= VECAKE_VOTING_POWER_BLOCK) { + return getVeVotingPower(account, blockNumber) + } const eligiblePools = await getActivePools(ChainId.BSC, Number(blockNumber)) const poolAddresses: Address[] = eligiblePools .filter((pair) => pair.stakingToken.address.toLowerCase() === bscTokens.cake.address.toLowerCase()) @@ -61,7 +68,7 @@ const useGetVotingPower = (block?: number): State & { isLoading: boolean; isErro ) if (error) console.error(error) - return { ...data, isLoading: status !== 'success', isError: status === 'error' } + return { total: 0, ...data, isLoading: status !== 'success', isError: status === 'error' } } export default useGetVotingPower diff --git a/apps/web/src/views/Voting/strategies.ts b/apps/web/src/views/Voting/strategies.ts index 9c15e311bc41d..61566fb046906 100644 --- a/apps/web/src/views/Voting/strategies.ts +++ b/apps/web/src/views/Voting/strategies.ts @@ -1,6 +1,35 @@ -const votePowerAddress = { +export const votePowerAddress = { v0: '0xc0FeBE244cE1ea66d27D23012B3D616432433F42', v1: '0x67Dfbb197602FDB9A9D305cC7A43b95fB63a0A56', + veCake: '0x67Dfbb197602FDB9A9D305cC7A43b95fB63a0A56', +} as const + +export const veCakeBalanceStrategy = { + name: 'contract-call', + params: { + address: votePowerAddress.veCake, + decimals: 18, + args: ['%{address}'], + methodABI: { + inputs: [ + { + internalType: 'address', + name: '_user', + type: 'address', + }, + ], + name: 'getVotingPowerWithoutPool', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + }, } export const cakeBalanceStrategy = (version: 'v0' | 'v1') => ({ diff --git a/packages/localization/src/config/translations.json b/packages/localization/src/config/translations.json index 9537a7142459e..92068f5e58cfe 100644 --- a/packages/localization/src/config/translations.json +++ b/packages/localization/src/config/translations.json @@ -3081,6 +3081,8 @@ "Your vote has been submitted successfully.": "Your vote has been submitted successfully.", "BETA": "BETA", "All Your DeFi Updates, All in One Place": "All Your DeFi Updates, All in One Place", + "Your voting power is determined by the number of veCAKE you have at the block detailed above. CAKE held in other places does NOT contribute to your voting power.": "Your voting power is determined by the number of veCAKE you have at the block detailed above. CAKE held in other places does NOT contribute to your voting power.", + "Once confirmed, voting action cannot be undone.": "Once confirmed, voting action cannot be undone.", "Web3 Notifications (BETA) trial available": "Web3 Notifications (BETA) trial available", "selected": "selected", "Collapse": "Collapse",