Skip to content

Commit

Permalink
Merge pull request #94 from aragon/feat/lock-to-veto-permit
Browse files Browse the repository at this point in the history
Lock to veto permit
  • Loading branch information
carlosgj94 authored Mar 15, 2024
2 parents e70ca5f + 599f458 commit c7b14d9
Show file tree
Hide file tree
Showing 9 changed files with 785 additions and 32 deletions.
526 changes: 526 additions & 0 deletions artifacts/ERC20Permit.sol.ts

Large diffs are not rendered by default.

109 changes: 109 additions & 0 deletions hooks/usePermit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useEffect } from "react";
import {
useReadContracts,
useSignTypedData,
useAccount,
} from "wagmi";
import { hexToSignature, Address } from "viem";
import { ERC20PermitAbi } from "@/artifacts/ERC20Permit.sol";
import { useAlertContext, AlertContextProps } from "@/context/AlertContext";
import { PUB_CHAIN, PUB_TOKEN_ADDRESS } from "@/constants";

export function usePermit() {
const { addAlert } = useAlertContext() as AlertContextProps;

const account_address = useAccount().address!;
const erc20Contract = {
address: PUB_TOKEN_ADDRESS,
abi: ERC20PermitAbi,
};
const { data: erc20data, refetch: erc20refetch } = useReadContracts({
contracts: [{
...erc20Contract,
functionName: "nonces",
args: [account_address],
},{
...erc20Contract,
functionName: "name",
},{
...erc20Contract,
functionName: "version",
}]
});
const [nonceResult, nameResult, versionResult] = erc20data || [];

const { signTypedDataAsync: permitSign, status: permitSignStatus, error: permitSignError } = useSignTypedData();

useEffect(() => {
switch (permitSignStatus) {
case "idle":
case "pending":
return;
case "error":
if (permitSignError?.message?.startsWith("User rejected the request")) {
addAlert("Transaction rejected by the user", {
timeout: 4 * 1000,
});
} else {
addAlert("Could not sign the permit", { type: "error", timeout: 1500 });
}
return;
case "success":
addAlert("Permit signed", { type: "success", timeout: 1500 });
return;
}
}, [permitSignStatus]);

const signPermit = async (dest: Address, value: BigInt, deadline: BigInt = BigInt(Math.floor(Date.now() / 1000) + 60 * 60)) => {
if (!nonceResult || !nameResult || !versionResult) return;

const nonce = BigInt(Number(nonceResult?.result));
const erc20_name = String(nameResult?.result);
/* We assume 1 if permit version is not specified */
const versionFromContract = String(versionResult?.result ?? '1');

const domain = {
chainId: PUB_CHAIN.id,
name: erc20_name,
version: versionFromContract,
verifyingContract: PUB_TOKEN_ADDRESS,
};

const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};

const message = {
owner: account_address,
spender: dest,
value,
nonce,
deadline,
};

try {
let sig = await permitSign({
account: account_address,
types,
domain,
primaryType: 'Permit',
message,
});

return hexToSignature(sig);
} catch (e) {
return;
}
};

return {
refetchPermitData: erc20refetch,
signPermit,
};
}
30 changes: 16 additions & 14 deletions plugins/lockToVote/components/proposal/details.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import dayjs from "dayjs";
import { compactNumber } from "@/utils/numbers";
import { ReactNode } from "react";
import { formatUnits } from "viem";

interface ProposalDetailsProps {
minVetoVotingPower?: bigint;
endDate?: bigint;
snapshotBlock?: bigint;
}

const ProposalDetails: React.FC<ProposalDetailsProps> = ({
/** Timestamp */
endDate,
snapshotBlock,
minVetoVotingPower,
}) => {
return (
<>
<Card>
<h2 className="text-xl flex-grow font-semibold text-neutral-600 pr-6">
Threshold
</h2>
<div className="items-right text-right flex-wrap">
<p className="text-neutral-600">Min. Quorum</p>
<span className="text-xl mr-2 font-semibold">
{compactNumber(formatUnits(minVetoVotingPower || BigInt(0), 18))}
</span>
</div>
</Card>
<Card>
<h2 className="text-xl flex-grow font-semibold pr-6 text-neutral-600">
Ending
Expand All @@ -27,23 +39,13 @@ const ProposalDetails: React.FC<ProposalDetailsProps> = ({
</p>
</div>
</Card>
<Card>
<h2 className="text-xl flex-grow font-semibold text-neutral-600 pr-6">
Snapshot
</h2>
<div className="items-right text-right flex-wrap">
<p className="text-neutral-600">Taken at block</p>
<span className="text-xl mr-2 font-semibold">
{snapshotBlock?.toLocaleString()}
</span>
</div>
</Card>

</>
);
};

// This should be encapsulated as soon as ODS exports this widget
const Card = function ({ children }: { children: ReactNode }) {
const Card = function({ children }: { children: ReactNode }) {
return (
<div
className="p-4 xl:p-6 w-full flex flex-col space-y-6
Expand Down
4 changes: 2 additions & 2 deletions plugins/lockToVote/components/vote/tally.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const VetoTally: FC<VoteTallyProps> = ({ voteCount, votePercentage }) => (
<Card>
<div className="space-between flex flex-row pb-2">
<p className={`text-primary-700 flex-grow text-xl font-semibold`}>
Vetoed
For
</p>
<p className="text-xl font-semibold">
{compactNumber(formatUnits(voteCount || BigInt(0), 18))}
Expand All @@ -27,7 +27,7 @@ const VetoTally: FC<VoteTallyProps> = ({ voteCount, votePercentage }) => (
);

// This should be encapsulated as soon as ODS exports this widget
const Card = function ({ children }: { children: ReactNode }) {
const Card = function({ children }: { children: ReactNode }) {
return (
<div
className="bg-neutral-0 box-border flex w-full flex-col space-y-6
Expand Down
93 changes: 93 additions & 0 deletions plugins/lockToVote/hooks/useProposalExecute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useEffect } from "react";
import {
useReadContract,
useWaitForTransactionReceipt,
useWriteContract,
} from "wagmi";
import { OptimisticTokenVotingPluginAbi } from "../artifacts/OptimisticTokenVotingPlugin.sol";
import { AlertContextProps, useAlertContext } from "@/context/AlertContext";
import { useRouter } from "next/router";
import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants";

export function useProposalExecute(proposalId: string) {
const { reload } = useRouter();
const { addAlert } = useAlertContext() as AlertContextProps;

const {
data: canExecute,
isError: isCanVoteError,
isLoading: isCanVoteLoading,
} = useReadContract({
address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS,
abi: OptimisticTokenVotingPluginAbi,
chainId: PUB_CHAIN.id,
functionName: "canExecute",
args: [proposalId],
});
const {
writeContract: executeWrite,
data: executeTxHash,
error: executingError,
status: executingStatus,
} = useWriteContract();
const { isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({ hash: executeTxHash });

const executeProposal = () => {
if (!canExecute) return;

executeWrite({
chainId: PUB_CHAIN.id,
abi: OptimisticTokenVotingPluginAbi,
address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS,
functionName: "execute",
args: [proposalId],
});
};

useEffect(() => {
if (executingStatus === "idle" || executingStatus === "pending") return;
else if (executingStatus === "error") {
if (executingError?.message?.startsWith("User rejected the request")) {
addAlert("Transaction rejected by the user", {
timeout: 4 * 1000,
});
} else {
console.error(executingError);
addAlert("Could not execute the proposal", {
type: "error",
description:
"The proposal may contain actions with invalid operations",
});
}
return;
}

// success
if (!executeTxHash) return;
else if (isConfirming) {
addAlert("Proposal submitted", {
description: "Waiting for the transaction to be validated",
type: "info",
txHash: executeTxHash,
});
return;
} else if (!isConfirmed) return;

addAlert("Proposal executed", {
description: "The transaction has been validated",
type: "success",
txHash: executeTxHash,
});

setTimeout(() => reload(), 1000 * 2);
}, [executingStatus, executeTxHash, isConfirming, isConfirmed]);

return {
executeProposal,
canExecute:
!isCanVoteError && !isCanVoteLoading && !isConfirmed && !!canExecute,
isConfirming,
isConfirmed,
};
}
8 changes: 4 additions & 4 deletions plugins/lockToVote/hooks/useProposalVariantStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ export const useProposalVariantStatus = (proposal: Proposal) => {
useEffect(() => {
if (!proposal || !proposal?.parameters) return;
setStatus(
proposal?.vetoTally >= proposal?.parameters?.minVetoVotingPower
? { variant: 'critical', label: 'Defeated' }
proposal?.vetoTally >= proposal?.parameters?.minVetoVotingPower
? { variant: 'success', label: 'Executable' }
: proposal?.active
? { variant: 'primary', label: 'Active' }
: proposal?.executed
: proposal?.executed
? { variant: 'success', label: 'Executed' }
: { variant: 'success', label: 'Executable' }
: { variant: 'critical', label: 'Defeated' }
);
}, [proposal?.vetoTally, proposal?.active, proposal?.executed, proposal?.parameters?.minVetoVotingPower]);

Expand Down
39 changes: 31 additions & 8 deletions plugins/lockToVote/hooks/useProposalVeto.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import {
usePublicClient,
useWaitForTransactionReceipt,
useWriteContract,
useReadContract,
useAccount,
} from "wagmi";
import { Address } from "viem";
import { ERC20PermitAbi } from "@/artifacts/ERC20Permit.sol";
import { useProposal } from "./useProposal";
import { useProposalVetoes } from "@/plugins/lockToVote/hooks/useProposalVetoes";
import { useUserCanVeto } from "@/plugins/lockToVote/hooks/useUserCanVeto";
import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol";
import { LockToVetoPluginAbi } from "@/plugins/lockToVote/artifacts/LockToVetoPlugin.sol";
import { usePermit } from "@/hooks/usePermit";
import { useAlertContext, AlertContextProps } from "@/context/AlertContext";
import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants";
import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol";
import { PUB_CHAIN, PUB_TOKEN_ADDRESS, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants";

export function useProposalVeto(proposalId: string) {
const publicClient = usePublicClient({ chainId: PUB_CHAIN.id });
Expand All @@ -26,8 +30,18 @@ export function useProposalVeto(proposalId: string) {
proposalId,
proposal
);
const { signPermit, refetchPermitData } = usePermit();

const { addAlert } = useAlertContext() as AlertContextProps;
const account_address = useAccount().address!;

const { data: balanceData } = useReadContract({
address: PUB_TOKEN_ADDRESS,
abi: ERC20PermitAbi,
functionName: "balanceOf",
args: [account_address],
});

const {
writeContract: vetoWrite,
data: vetoTxHash,
Expand Down Expand Up @@ -70,14 +84,23 @@ export function useProposalVeto(proposalId: string) {
});
refetchCanVeto();
refetchProposal();
refetchPermitData();
}, [vetoingStatus, vetoTxHash, isConfirming, isConfirmed]);

const vetoProposal = () => {
vetoWrite({
abi: LockToVetoPluginAbi,
address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS,
functionName: "veto",
args: [proposalId, 50000000000000000000],
let dest: Address = PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS;
let value = BigInt(Number(balanceData));
let deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 60); // 1 hour from now

signPermit(dest, value, deadline).then((sig) => {
if (!sig) return;

vetoWrite({
abi: LockToVetoPluginAbi,
address: dest,
functionName: "vetoPermit",
args: [proposalId, value, deadline, sig.v, sig.r, sig.s],
});
});
};

Expand Down
4 changes: 2 additions & 2 deletions plugins/lockToVote/pages/proposal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PleaseWaitSpinner } from "@/components/please-wait";
import { useSkipFirstRender } from "@/hooks/useSkipFirstRender";
import { useState } from "react";
import { useProposalVeto } from "@/plugins/lockToVote/hooks/useProposalVeto";
import { useProposalExecute } from "@/plugins/dualGovernance/hooks/useProposalExecute";
import { useProposalExecute } from "@/plugins/lockToVote/hooks/useProposalExecute";
import { useProposalClaimLock } from "@/plugins/lockToVote/hooks/useProposalClaimLock";
import { useAccount } from "wagmi";

Expand Down Expand Up @@ -86,7 +86,7 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) {
/>
<ProposalDetails
endDate={proposal?.parameters?.endDate}
snapshotBlock={proposal?.parameters?.snapshotBlock}
minVetoVotingPower={proposal?.parameters?.minVetoVotingPower}
/>
</div>
<div className="py-12 w-full">
Expand Down
Loading

0 comments on commit c7b14d9

Please sign in to comment.