Skip to content

Commit

Permalink
Merge pull request #1343 from decent-dao/feature/improve-quorum-and-v…
Browse files Browse the repository at this point in the history
…oting-power

Quorum / Voting power adjustments
  • Loading branch information
mudrila authored Feb 8, 2024
2 parents 08bf022 + b843328 commit e4166d5
Show file tree
Hide file tree
Showing 16 changed files with 354 additions and 216 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"@apollo/client": "^3.7.10",
"@chakra-ui/next-js": "^2.2.0",
"@chakra-ui/react": "^2.8.2",
"@decent-org/fractal-ui": "^0.1.22",
"@decent-org/fractal-ui": "^0.1.23",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@ethersproject/abstract-signer": "^5.7.0",
Expand Down
140 changes: 105 additions & 35 deletions src/components/Proposals/ProposalSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Text, Box, Divider, Flex } from '@chakra-ui/react';
import { Text, Box, Button, Divider, Flex, Tooltip } from '@chakra-ui/react';
import { ArrowAngleUp } from '@decent-org/fractal-ui';
import { format } from 'date-fns';
import { BigNumber } from 'ethers';
import { useMemo } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common';
import useBlockTimestamp from '../../hooks/utils/useBlockTimestamp';
Expand All @@ -11,12 +12,14 @@ import { DEFAULT_DATE_TIME_FORMAT } from '../../utils/numberFormats';
import ContentBox from '../ui/containers/ContentBox';
import { DisplayAddress } from '../ui/links/DisplayAddress';
import DisplayTransaction from '../ui/links/DisplayTransaction';
import EtherscanLinkBlock from '../ui/links/EtherscanLinkBlock';
import { InfoBoxLoader } from '../ui/loaders/InfoBoxLoader';
import { ExtendedProgressBar } from '../ui/utils/ProgressBar';
import { QuorumProgressBar } from '../ui/utils/ProgressBar';
import { InfoRow } from './MultisigProposalDetails/TxDetails';

export default function ProposalSummary({
proposal: {
eventDate,
startBlock,
votesSummary: { yes, no, abstain },
deadlineMs,
Expand All @@ -26,12 +29,19 @@ export default function ProposalSummary({
}: {
proposal: AzoriusProposal;
}) {
const { governance } = useFractal();
const {
governance,
governanceContracts: { tokenContract },
readOnly: {
user: { votingWeight, address },
},
} = useFractal();

const azoriusGovernance = governance as AzoriusGovernance;
const { votesToken, type, erc721Tokens, votingStrategy } = azoriusGovernance;
const { t } = useTranslation(['proposal', 'common', 'navigation']);
const startBlockTimeStamp = useBlockTimestamp(startBlock.toNumber());
const [proposalsERC20VotingWeight, setProposalsERC20VotingWeight] = useState('0');
const totalVotesCasted = useMemo(() => yes.add(no).add(abstain), [yes, no, abstain]);
const totalVotingWeight = useMemo(
() =>
Expand All @@ -41,20 +51,26 @@ export default function ProposalSummary({
),
[erc721Tokens]
);
const getVotesPercentage = (voteTotal: BigNumber): number => {
if (type === GovernanceType.AZORIUS_ERC20) {
if (!votesToken?.totalSupply || votesToken.totalSupply.eq(0)) {
return 0;
}
return voteTotal.div(votesToken.totalSupply.div(100)).toNumber();
} else if (type === GovernanceType.AZORIUS_ERC721) {
if (totalVotesCasted.eq(0) || !erc721Tokens || !totalVotingWeight) {
return 0;
const votesTokenDecimalsDenominator = useMemo(
() => BigNumber.from(10).pow(votesToken?.decimals || 0),
[votesToken?.decimals]
);
const [showVotingPower, setShowVotingPower] = useState(false);

const toggleShowVotingPower = () => setShowVotingPower(prevState => !prevState);

useEffect(() => {
async function loadProposalVotingWeight() {
if (tokenContract && address) {
const pastVotingWeight = await tokenContract.asSigner.getPastVotes(address, startBlock);
setProposalsERC20VotingWeight(
pastVotingWeight.div(votesTokenDecimalsDenominator).toString()
);
}
return voteTotal.mul(100).div(totalVotingWeight).toNumber();
}
return 0;
};

loadProposalVotingWeight();
}, [address, startBlock, tokenContract, votesTokenDecimalsDenominator]);

const isERC20 = type === GovernanceType.AZORIUS_ERC20;
const isERC721 = type === GovernanceType.AZORIUS_ERC721;
Expand All @@ -69,29 +85,54 @@ export default function ProposalSummary({
);
}

const yesVotesPercentage = getVotesPercentage(yes);
const noVotesPercentage = getVotesPercentage(no);
const strategyQuorum =
votesToken && isERC20
? votingStrategy.quorumPercentage!.value.toNumber()
: isERC721
? votingStrategy.quorumThreshold!.value.toNumber()
: 1;
const requiredVotesPercentageToPass = Math.max(
isERC721
? no
.mul(100)
.div(totalVotingWeight || 1)
.toNumber() + 1
: noVotesPercentage + 1,
isERC721 ? (strategyQuorum * 100) / totalVotingWeight!.toNumber() : strategyQuorum
const reachedQuorum = isERC721
? totalVotesCasted.sub(no).toString()
: votesToken
? totalVotesCasted.sub(no).div(votesTokenDecimalsDenominator).toString()
: '0';
const totalQuorum = isERC721
? strategyQuorum.toString()
: votesToken?.totalSupply
.div(votesTokenDecimalsDenominator)
.div(100)
.mul(strategyQuorum)
.toString();

const ShowVotingPowerButton = (
<Button
pr={0}
py={0}
height="auto"
justifyContent="flex-end"
alignItems="flex-start"
variant="link"
textStyle="text-base-sans-regular"
color="gold.500"
onClick={toggleShowVotingPower}
>
{showVotingPower
? isERC721
? votingWeight.toString()
: proposalsERC20VotingWeight
: t('show')}
</Button>
);

return (
<ContentBox containerBoxProps={{ bg: BACKGROUND_SEMI_TRANSPARENT }}>
<Text textStyle="text-lg-mono-medium">{t('proposalSummaryTitle')}</Text>
<Box marginTop={4}>
<Divider color="chocolate.700" />
<InfoRow
property={t('votingSystem')}
value={t('singleSnapshotVotingSystem')}
/>
<InfoRow
property={t('proposalSummaryStartDate')}
value={format(startBlockTimeStamp * 1000, DEFAULT_DATE_TIME_FORMAT)}
Expand All @@ -102,7 +143,6 @@ export default function ProposalSummary({
/>
<Flex
marginTop={4}
marginBottom={4}
justifyContent="space-between"
>
<Text
Expand All @@ -113,6 +153,37 @@ export default function ProposalSummary({
</Text>
<DisplayAddress address={proposer} />
</Flex>
<Flex
marginTop={4}
justifyContent="space-between"
>
<Text
textStyle="text-base-sans-regular"
color="chocolate.200"
>
{t('snapshotTaken')}
</Text>
<EtherscanLinkBlock blockNumber={startBlock.toString()}>
{format(eventDate, DEFAULT_DATE_TIME_FORMAT)} <ArrowAngleUp />
</EtherscanLinkBlock>
</Flex>
<Flex
marginTop={4}
marginBottom={transactionHash ? 0 : 4}
justifyContent="space-between"
>
<Text
textStyle="text-base-sans-regular"
color="chocolate.200"
>
{t('votingPower')}
</Text>
{showVotingPower ? (
<Tooltip label={t('votingPowerTooltip')}>{ShowVotingPowerButton}</Tooltip>
) : (
ShowVotingPowerButton
)}
</Flex>
{transactionHash && (
<Flex
marginTop={4}
Expand All @@ -131,23 +202,22 @@ export default function ProposalSummary({
<Divider color="chocolate.700" />
</Box>
<Box marginTop={4}>
<ExtendedProgressBar
label={t('support')}
<QuorumProgressBar
helperText={t(
isERC20
? 'proposalSupportERC20SummaryHelper'
: isERC721
? 'proposalSupportERC721SummaryHelper'
: '',
{
count: isERC721
? Math.max(no.toNumber() || strategyQuorum)
: requiredVotesPercentageToPass,
quorum: strategyQuorum,
total: isERC721
? totalVotingWeight?.toString()
: votesToken?.totalSupply.div(votesTokenDecimalsDenominator).toString(),
}
)}
valueLabel={isERC721 ? `${yes.toString()}/${totalVotingWeight?.toString()}` : undefined}
percentage={yesVotesPercentage}
requiredPercentage={requiredVotesPercentageToPass}
reachedQuorum={reachedQuorum}
totalQuorum={totalQuorum}
unit={isERC20 ? '%' : ''}
/>
</Box>
Expand Down
25 changes: 13 additions & 12 deletions src/components/Proposals/ProposalVotes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,20 @@ export function VotesPercentage({
flexWrap="wrap"
marginTop={2}
>
<Flex
justifyContent="space-between"
width="100%"
>
<Text
marginTop={2}
marginBottom={2}
<ProgressBar value={percentage}>
<Flex
justifyContent="space-between"
width="100%"
>
{label}
</Text>
{children}
</Flex>
<ProgressBar value={percentage} />
<Text
marginTop={2}
marginBottom={2}
>
{label}
</Text>
{children}
</Flex>
</ProgressBar>
</Flex>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Text, Box, Divider, Flex } from '@chakra-ui/react';
import { Text, Box, Button, Divider, Flex, Tooltip } from '@chakra-ui/react';
import { format } from 'date-fns';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { BACKGROUND_SEMI_TRANSPARENT } from '../../../constants/common';
import { ExtendedSnapshotProposal } from '../../../types';
import { DEFAULT_DATE_TIME_FORMAT } from '../../../utils/numberFormats';
import ContentBox from '../../ui/containers/ContentBox';
import ExternalLink from '../../ui/links/ExternalLink';
import { InfoBoxLoader } from '../../ui/loaders/InfoBoxLoader';
import { ExtendedProgressBar } from '../../ui/utils/ProgressBar';
import { QuorumProgressBar } from '../../ui/utils/ProgressBar';
import { InfoRow } from '../MultisigProposalDetails/TxDetails';
import useSnapshotUserVotingWeight from './hooks/useSnapshotUserVotingWeight';
import useTotalVotes from './hooks/useTotalVotes';

interface ISnapshotProposalSummary {
Expand All @@ -18,6 +20,10 @@ interface ISnapshotProposalSummary {
export default function SnapshotProposalSummary({ proposal }: ISnapshotProposalSummary) {
const { t } = useTranslation(['proposal', 'common', 'navigation']);
const { totalVotesCasted } = useTotalVotes({ proposal });
const { votingWeight } = useSnapshotUserVotingWeight({ proposal });
const [showVotingPower, setShowVotingPower] = useState(false);

const toggleShowVotingPower = () => setShowVotingPower(prevState => !prevState);

if (!proposal) {
return (
Expand All @@ -40,6 +46,22 @@ export default function SnapshotProposalSummary({ proposal }: ISnapshotProposalS
}
};

const ShowVotingPowerButton = (
<Button
pr={0}
py={0}
height="auto"
justifyContent="flex-end"
alignItems="flex-start"
variant="link"
textStyle="text-base-sans-regular"
color="gold.500"
onClick={toggleShowVotingPower}
>
{showVotingPower ? votingWeight : t('show')}
</Button>
);

return (
<ContentBox containerBoxProps={{ bg: BACKGROUND_SEMI_TRANSPARENT }}>
<Text textStyle="text-lg-mono-medium">{t('proposalSummaryTitle')}</Text>
Expand Down Expand Up @@ -71,19 +93,32 @@ export default function SnapshotProposalSummary({ proposal }: ISnapshotProposalS
property={t('proposalSummaryEndDate')}
value={format(proposal.endTime * 1000, DEFAULT_DATE_TIME_FORMAT)}
/>
<Flex
marginTop={4}
marginBottom={4}
justifyContent="space-between"
>
<Text
textStyle="text-base-sans-regular"
color="chocolate.200"
>
{t('votingPower')}
</Text>
{showVotingPower ? (
<Tooltip label={t('votingPowerTooltip')}>{ShowVotingPowerButton}</Tooltip>
) : (
ShowVotingPowerButton
)}
</Flex>
<Divider
color="chocolate.700"
marginTop={4}
/>
{!!proposal.quorum && (
<Box marginTop={4}>
<ExtendedProgressBar
label={t('quorum', { ns: 'common' })}
valueLabel={`${totalVotesCasted}/${proposal.quorum}`}
percentage={
proposal.quorum > 0 ? (totalVotesCasted / proposal.quorum || 1) * 100 : 100
}
requiredPercentage={100}
<QuorumProgressBar
reachedQuorum={totalVotesCasted.toString()}
totalQuorum={proposal.quorum?.toString()}
unit=""
/>
</Box>
Expand Down
Loading

0 comments on commit e4166d5

Please sign in to comment.