Skip to content

Commit

Permalink
Build calls with permit2 (#74)
Browse files Browse the repository at this point in the history
* Update swaps to use buildCallWithPermit2

* use build call with permit2 for add liqiuidity

* Fix add liquidity proportional queryoutput rounding error
  • Loading branch information
MattPereira authored Sep 4, 2024
1 parent 7c191d9 commit 41c1c55
Show file tree
Hide file tree
Showing 18 changed files with 348 additions and 239 deletions.
4 changes: 2 additions & 2 deletions packages/nextjs/app/hooks/HookDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const HooksDetails = ({ hooks }: { hooks: HookInfo[] }) => {

<div className="text-xl">{hook.description}</div>

<div className="flex justify-between">
<div className="flex flex-col md:flex-row flex-wrap md:justify-between">
<div>Audited: {hook.audited}</div>
<div>Categories: {categories}</div>
<Link
Expand Down Expand Up @@ -51,7 +51,7 @@ export const HooksDetails = ({ hooks }: { hooks: HookInfo[] }) => {

<div className="hidden lg:flex text-center">
<Link
className="hover:underline flex gap-2 items-center text-nowrap overflow-hidden whitespace-nowrap"
className="hover:underline hover:text-accent flex gap-2 items-center text-nowrap overflow-hidden whitespace-nowrap"
target="_blank"
rel="noopener noreferrer"
href={hook.github}
Expand Down
6 changes: 2 additions & 4 deletions packages/nextjs/app/hooks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@ const Hooks: NextPage = async () => {
<div className="mb-7 w-full text-center">
<h1 className="text-3xl md:text-5xl font-bold mb-7 text-center">Pool Hooks</h1>
<div className="text-xl my-10">
Extend the functionality of liquidity pools with hooks contracts. Consider utilizing one of the examples below
or{" "}
Extend the functionality of liquidity pools with hooks contracts. Use one of our curated examples below or{" "}
<Link
target="_blank"
rel="noopener noreferrer"
href="https://balancer-hooks.vercel.app/submit-hook.html"
className="link"
>
submit your own creation
submit your own
</Link>
.
</div>
</div>
<div className="w-full flex flex-col gap-3">
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const Home: NextPage = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 w-full">
{TOOLS.map(item => (
<Link
className="relative bg-base-200 hover:scale-105 hover:bg-neutral text-2xl text-center p-8 rounded-3xl shadow-lg"
className="relative bg-base-200 hover:shadow-inner text-2xl text-center p-8 rounded-3xl shadow-lg"
key={item.href}
href={item.href}
passHref
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { useState } from "react";
import React, { useCallback, useState } from "react";
import { ResultsDisplay, TokenField, TransactionButton } from ".";
import { InputAmount, calculateProportionalAmounts } from "@balancer/sdk";
import { InputAmount, PERMIT2, calculateProportionalAmounts, erc20Abi } from "@balancer/sdk";
import { useQueryClient } from "@tanstack/react-query";
import debounce from "lodash.debounce";
import { formatUnits, parseUnits } from "viem";
import { useContractEvent } from "wagmi";
import { useContractEvent, useContractRead } from "wagmi";
import { Alert } from "~~/components/common/";
import abis from "~~/contracts/abis";
import { useAddLiquidity, useQueryAddLiquidity } from "~~/hooks/balancer/";
import { useAddLiquidity, useQueryAddLiquidity, useTargetFork } from "~~/hooks/balancer/";
import { PoolActionsProps, PoolOperationReceipt, TokenAmountDetails } from "~~/hooks/balancer/types";
import { useApproveTokens, useReadTokens } from "~~/hooks/token/";
import { useAllowancesOnTokens, useApproveOnToken } from "~~/hooks/token/";

/**
* 1. Query adding some amount of liquidity to the pool
Expand All @@ -30,21 +31,36 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
const [tokenInputs, setTokenInputs] = useState<InputAmount[]>(initialTokenInputs);
const [addLiquidityReceipt, setAddLiquidityReceipt] = useState<PoolOperationReceipt>(null);
const [referenceAmount, setReferenceAmount] = useState<InputAmount>(); // only for the proportional add liquidity case
const [isCalculatingProportional, setIsCalculatingProportional] = useState(false);

const queryClient = useQueryClient();
const {
data: queryResponse,
isFetching: isQueryFetching,
error: queryError,
refetch: refetchQueryAddLiquidity,
} = useQueryAddLiquidity(pool, tokenInputs, referenceAmount);
const { sufficientAllowances, isApproving, approveTokens } = useApproveTokens(tokenInputs);
const { mutate: addLiquidity, isPending: isAddLiquidityPending, error: addLiquidityError } = useAddLiquidity();
const { refetchTokenAllowances } = useReadTokens(tokenInputs);
const queryClient = useQueryClient();
const { tokensToApprove, refetchTokenAllowances } = useAllowancesOnTokens(tokenInputs);
const {
mutate: addLiquidity,
isPending: isAddLiquidityPending,
error: addLiquidityError,
} = useAddLiquidity(tokenInputs);

// Delay update of token inputs so user has time to finish typing numbers longer than 1 digit
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedSetTokenInputs = useCallback(
debounce(updatedTokens => {
setTokenInputs(updatedTokens);
setIsCalculatingProportional(false);
}, 1000),
[],
);

const handleInputChange = (index: number, value: string) => {
queryClient.removeQueries({ queryKey: ["queryAddLiquidity"] });
setAddLiquidityReceipt(null);

const updatedTokens = tokenInputs.map((token, idx) => {
if (idx === index) {
return { ...token, rawAmount: parseUnits(value, token.decimals) };
Expand All @@ -64,10 +80,12 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
})),
};

setIsCalculatingProportional(true);
const referenceAmount = updatedTokens[index];
const { bptAmount, tokenAmounts } = calculateProportionalAmounts(poolStateWithBalances, referenceAmount);
setReferenceAmount(bptAmount);
setTokenInputs(tokenAmounts);
const { tokenAmounts } = calculateProportionalAmounts(poolStateWithBalances, referenceAmount);
setReferenceAmount(referenceAmount);
setTokenInputs(updatedTokens);
debouncedSetTokenInputs(tokenAmounts);
} else {
setTokenInputs(updatedTokens);
}
Expand Down Expand Up @@ -105,7 +123,7 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
},
});

const error = queryError || addLiquidityError;
const error: Error | null = queryError || addLiquidityError;
const isFormEmpty = tokenInputs.some(token => token.rawAmount === 0n);

return (
Expand All @@ -129,10 +147,10 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
label="Query"
onClick={handleQueryAddLiquidity}
isDisabled={isQueryFetching}
isFormEmpty={isFormEmpty}
isFormEmpty={isFormEmpty || isCalculatingProportional}
/>
) : !sufficientAllowances ? (
<TransactionButton label="Approve" isDisabled={isApproving} onClick={approveTokens} />
) : tokensToApprove.length > 0 ? (
<ApproveButtons tokens={tokensToApprove} refetchTokenAllowances={refetchTokenAllowances} />
) : (
<TransactionButton label="Add Liquidity" isDisabled={isAddLiquidityPending} onClick={handleAddLiquidity} />
)}
Expand All @@ -159,7 +177,42 @@ export const AddLiquidityForm: React.FC<PoolActionsProps> = ({
/>
)}

{(error as Error) && <Alert type="error">{(error as Error).message}</Alert>}
{error && <Alert type="error">{error.message}</Alert>}
</section>
);
};

const ApproveButtons = ({
tokens,
refetchTokenAllowances,
}: {
tokens: InputAmount[];
refetchTokenAllowances: () => void;
}) => {
const { chainId } = useTargetFork();
const token = tokens[0];

const { data: symbol } = useContractRead({
address: token.address,
abi: erc20Abi,
functionName: "symbol",
});

const {
mutateAsync: approve,
isPending: isApprovePending,
error: approveError,
} = useApproveOnToken(token.address, PERMIT2[chainId]);

const handleApprove = async () => {
await approve();
refetchTokenAllowances();
};

return (
<div>
<TransactionButton label={`Approve ${symbol}`} isDisabled={isApprovePending} onClick={handleApprove} />
{approveError && <Alert type="error">{approveError.message}</Alert>}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useAccount } from "wagmi";
import { Alert } from "~~/components/common";
import { Pool, RefetchPool, TokenBalances } from "~~/hooks/balancer";
import { useAccountBalance, useScaffoldContractWrite } from "~~/hooks/scaffold-eth";
import { useReadTokens } from "~~/hooks/token";
import { useTokenBalancesOfUser } from "~~/hooks/token";

type Operation = "Swap" | "AddLiquidity" | "RemoveLiquidity";

Expand All @@ -14,13 +14,7 @@ type Operation = "Swap" | "AddLiquidity" | "RemoveLiquidity";
export const PoolOperations: React.FC<{ pool: Pool; refetchPool: RefetchPool }> = ({ pool, refetchPool }) => {
const [activeTab, setActiveTab] = useState<Operation>("Swap");

const tokens = pool.poolTokens.map(token => ({
address: token.address as `0x${string}`,
decimals: token.decimals,
rawAmount: 0n, // Quirky solution cus useReadTokens expects type InputAmount[] cus originally built for AddLiquidityForm :D
}));

const { tokenBalances, refetchTokenBalances } = useReadTokens(tokens);
const { tokenBalances, refetchTokenBalances } = useTokenBalancesOfUser(pool.poolTokens);

const tabs = {
Swap: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const RemoveLiquidityForm: React.FC<PoolActionsProps> = ({ pool, refetchP
},
});

const error = queryError || removeLiquidityError || approveError;
const error: Error | null = queryError || removeLiquidityError || approveError;
const isFormEmpty = bptInput.displayValue === "";
const isSufficientAllowance = allowance !== undefined && allowance >= bptInput.rawAmount;

Expand All @@ -112,7 +112,7 @@ export const RemoveLiquidityForm: React.FC<PoolActionsProps> = ({ pool, refetchP
{!queryResponse || removeLiquidityReceipt || isFormEmpty ? (
<TransactionButton label="Query" onClick={handleQuery} isDisabled={isQueryFetching} isFormEmpty={isFormEmpty} />
) : !isSufficientAllowance ? (
<TransactionButton label="Approve" isDisabled={isApprovePending} onClick={handleApprove} />
<TransactionButton label={`Approve ${pool.symbol}`} isDisabled={isApprovePending} onClick={handleApprove} />
) : (
<TransactionButton
label="Remove Liquidity"
Expand Down Expand Up @@ -141,7 +141,7 @@ export const RemoveLiquidityForm: React.FC<PoolActionsProps> = ({ pool, refetchP
/>
)}

{(error as Error) && <Alert type="error">{(error as Error).message}</Alert>}
{error && <Alert type="error">{error.message}</Alert>}
</section>
);
};
42 changes: 13 additions & 29 deletions packages/nextjs/app/pools/_components/operations/SwapForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useContractEvent } from "wagmi";
import { Alert } from "~~/components/common";
import { useQuerySwap, useSwap, useTargetFork } from "~~/hooks/balancer/";
import { PoolActionsProps, PoolOperationReceipt, SwapConfig } from "~~/hooks/balancer/types";
import { useAllowanceOnPermit2, useAllowanceOnToken, useApproveOnPermit2, useApproveOnToken } from "~~/hooks/token";
import { useAllowanceOnToken, useApproveOnToken } from "~~/hooks/token";

const initialSwapConfig = {
tokenIn: {
Expand Down Expand Up @@ -62,21 +62,15 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
error: queryError,
refetch: refetchQuerySwap,
} = useQuerySwap(swapInput, setSwapConfig);
const { data: allowanceOnPermit2, refetch: refetchAllowanceOnPermit2 } = useAllowanceOnPermit2(tokenIn.address);
const { data: allowanceOnToken, refetch: refetchAllowanceOnToken } = useAllowanceOnToken(
tokenIn.address,
PERMIT2[chainId],
);
const {
mutateAsync: approveRouter,
isPending: isApproveRouterPending,
error: approveRouterError,
mutateAsync: approveOnToken,
isPending: isApprovePending,
error: approveError,
} = useApproveOnToken(tokenIn.address, PERMIT2[chainId]);
const {
mutateAsync: approvePermit2,
isPending: isApprovePermit2Pending,
error: approvePermit2Error,
} = useApproveOnPermit2(tokenIn.address);
const { mutate: swap, isPending: isSwapPending, error: swapError } = useSwap(swapInput);

const handleQuerySwap = async () => {
Expand All @@ -86,14 +80,8 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
};

const handleApprove = async () => {
if (allowanceOnPermit2 && allowanceOnPermit2[0] < swapConfig.tokenIn.rawAmount) {
if (allowanceOnToken !== undefined && allowanceOnToken < swapConfig.tokenIn.rawAmount) {
await approveRouter();
refetchAllowanceOnToken();
}
await approvePermit2();
refetchAllowanceOnPermit2();
}
await approveOnToken();
refetchAllowanceOnToken();
};

const handleSwap = async () => {
Expand Down Expand Up @@ -125,10 +113,6 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
}));
};

const sufficientAllowance = useMemo(() => {
return allowanceOnPermit2 && allowanceOnPermit2[0] >= swapConfig.tokenIn.rawAmount;
}, [allowanceOnPermit2, swapConfig.tokenIn.rawAmount]);

useContractEvent({
address: VAULT_V3[chainId],
abi: vaultV3Abi,
Expand All @@ -153,8 +137,12 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
},
});

const sufficientAllowance = useMemo(() => {
return allowanceOnToken && allowanceOnToken >= swapConfig.tokenIn.rawAmount;
}, [allowanceOnToken, swapConfig.tokenIn.rawAmount]);

const isFormEmpty = swapConfig.tokenIn.amount === "" && swapConfig.tokenOut.amount === "";
const error = queryError || swapError || approveRouterError || approvePermit2Error;
const error: Error | null = queryError || swapError || approveError;

return (
<section className="flex flex-col gap-5">
Expand Down Expand Up @@ -189,16 +177,12 @@ export const SwapForm: React.FC<PoolActionsProps> = ({ pool, refetchPool, tokenB
isFormEmpty={isFormEmpty}
/>
) : !sufficientAllowance ? (
<TransactionButton
label="Approve"
isDisabled={isApprovePermit2Pending || isApproveRouterPending}
onClick={handleApprove}
/>
<TransactionButton label={`Approve ${tokenIn.symbol}`} isDisabled={isApprovePending} onClick={handleApprove} />
) : (
<TransactionButton label="Swap" isDisabled={isSwapPending} onClick={handleSwap} />
)}

{(error as Error) && <Alert type="error">{(error as Error).message} / </Alert>}
{error && <Alert type="error">{error.message} / </Alert>}

{queryResponse && (
<ResultsDisplay
Expand Down
12 changes: 11 additions & 1 deletion packages/nextjs/contracts/externalContracts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
BALANCER_BATCH_ROUTER,
BALANCER_ROUTER,
PERMIT2,
VAULT_V3,
balancerBatchRouterAbi,
balancerRouterAbi,
vaultExtensionV3Abi, // balancerBatchRouterAbi // Batch Router not exported from balancer sdk?
permit2Abi,
vaultExtensionV3Abi,
} from "@balancer/sdk";
import { sepolia } from "viem/chains";
import scaffoldConfig from "~~/scaffold.config";
Expand Down Expand Up @@ -40,6 +42,10 @@ const externalContracts = {
address: BALANCER_BATCH_ROUTER[scaffoldConfig.targetFork.id],
abi: balancerBatchRouterAbi,
},
Permit2: {
address: PERMIT2[scaffoldConfig.targetFork.id],
abi: permit2Abi,
},
},
11155111: {
Vault: {
Expand All @@ -54,6 +60,10 @@ const externalContracts = {
address: BALANCER_BATCH_ROUTER[sepolia.id],
abi: balancerBatchRouterAbi,
},
Permit2: {
address: PERMIT2[sepolia.id],
abi: permit2Abi,
},
},
} as const;

Expand Down
Loading

0 comments on commit 41c1c55

Please sign in to comment.