Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CoW AMM: Price impact information on deposit and creation of AMMs #703

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { Address } from "viem";
import { useAccount } from "wagmi";
import { z } from "zod";

import { Button } from "#/components";
import { AlertCard } from "#/components/AlertCard";
import { Input } from "#/components/Input";
import { PriceOracleForm } from "#/components/PriceOracleForm";
import { TokenAmountInput } from "#/components/TokenAmountInput";
Expand All @@ -20,11 +21,13 @@ import {
} from "#/components/ui/accordion";
import { Form } from "#/components/ui/form";
import { useManagedTransaction } from "#/hooks/tx-manager/useManagedTransaction";
import { useDebounce } from "#/hooks/useDebounce";
import { ConstantProductFactoryABI } from "#/lib/abis/ConstantProductFactory";
import { UNBALANCED_USD_DIFF_THRESHOLD } from "#/lib/constants";
import { COW_CONSTANT_PRODUCT_FACTORY } from "#/lib/contracts";
import { IToken } from "#/lib/fetchAmmData";
import { ammFormSchema } from "#/lib/schema";
import { getNewMinTradeToken0 } from "#/lib/tokenUtils";
import { fetchTokenUsdPrice, getNewMinTradeToken0 } from "#/lib/tokenUtils";
import { buildTxCreateAMMArgs } from "#/lib/transactionFactory";
import { cn } from "#/lib/utils";
import { ChainId, publicClientsFromIds } from "#/utils/chainsPublicClients";
Expand Down Expand Up @@ -75,6 +78,13 @@ export function CreateAMMForm({ userId }: { userId: string }) {
writeContract(buildTxCreateAMMArgs({ data }));
}
};
const [token0UsdPrice, setToken0UsdPrice] = useState<number>();
const [token1UsdPrice, setToken1UsdPrice] = useState<number>();
const [amountUsdDiff, setAmountUsdDiff] = useState<number>();
const debouncedAmountUsdDiff = useDebounce<number | undefined>(
amountUsdDiff,
300,
);

useEffect(() => {
setValue("safeAddress", safeAddress as string);
Expand All @@ -99,6 +109,38 @@ export function CreateAMMForm({ userId }: { userId: string }) {
onTxStatusFinal();
}
}, [status]);
async function updateTokenUsdPrice(
token: IToken,
setAmountUsd: (value: number) => void,
) {
const amountUsd = await fetchTokenUsdPrice({
chainId: chainId as ChainId,
tokenDecimals: token.decimals,
tokenAddress: token.address as Address,
});
setAmountUsd(amountUsd);
}

useEffect(() => {
if (token0) {
updateTokenUsdPrice(token0, setToken0UsdPrice);
}
}, [token0]);

useEffect(() => {
if (token1) {
updateTokenUsdPrice(token1, setToken1UsdPrice);
}
}, [token1]);

useEffect(() => {
if (token0?.address == token1?.address) return;
if (token0UsdPrice && token1UsdPrice && amount0 && amount1) {
const amount0Usd = amount0 * token0UsdPrice;
const amount1Usd = amount1 * token1UsdPrice;
setAmountUsdDiff(Math.abs(amount0Usd - amount1Usd));
}
}, [amount0, amount1, token0UsdPrice, token1UsdPrice]);

return (
// @ts-ignore
Expand Down Expand Up @@ -183,6 +225,18 @@ export function CreateAMMForm({ userId }: { userId: string }) {
</AccordionItem>
</Accordion>

<span>
{amountUsdDiff} {debouncedAmountUsdDiff}
</span>
{(debouncedAmountUsdDiff || 0) > UNBALANCED_USD_DIFF_THRESHOLD && (
<AlertCard title="Unbalanced amounts" style="warning">
<p>
The difference between the USD value of the two token amounts is
greater than $5000. This may lead to an unbalanced AMM and result in
loss of funds.
</p>
</AlertCard>
)}
<div className="flex justify-center gap-x-5 mt-2">
<Button
loading={
Expand Down
35 changes: 32 additions & 3 deletions apps/cow-amm-deployer/src/components/DepositForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { toast } from "@bleu/ui";
import { formatNumber, toast } from "@bleu/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, useWatch } from "react-hook-form";
import { z } from "zod";
Expand All @@ -9,10 +9,17 @@ import { Button } from "#/components";
import { TokenInfo } from "#/components/TokenInfo";
import { Form, FormMessage } from "#/components/ui/form";
import { useManagedTransaction } from "#/hooks/tx-manager/useManagedTransaction";
import { useDebounce } from "#/hooks/useDebounce";
import {
PRICE_IMPACT_THRESHOLD,
USD_VALUE_FOR_PRICE_IMPACT_WARNING,
} from "#/lib/constants";
import { ICowAmm } from "#/lib/fetchAmmData";
import { calculatePriceImpact } from "#/lib/priceImpact";
import { getDepositSchema } from "#/lib/schema";
import { buildDepositAmmArgs } from "#/lib/transactionFactory";

import { AlertCard } from "./AlertCard";
import { TokenAmountInput } from "./TokenAmountInput";

export function DepositForm({
Expand All @@ -26,7 +33,7 @@ export function DepositForm({
}) {
const schema = getDepositSchema(
Number(walletBalanceToken0),
Number(walletBalanceToken1),
Number(walletBalanceToken1)
);

const form = useForm<z.input<typeof schema>>({
Expand All @@ -46,6 +53,18 @@ export function DepositForm({
control,
name: ["amount0", "amount1"],
});
const depositUsdValue =
ammData.token0.usdPrice * amount0 + ammData.token1.usdPrice * amount1;

const priceImpact = calculatePriceImpact({
balance0: Number(ammData.token0.balance),
balance1: Number(ammData.token1.balance),
amount0: Number(amount0),
amount1: Number(amount1),
});

const debouncedPriceImpact = useDebounce<number>(priceImpact, 300);
const debouncedDepositUsdValue = useDebounce<number>(depositUsdValue, 300);

const onSubmit = async (data: z.output<typeof schema>) => {
const txArgs = buildDepositAmmArgs({
Expand Down Expand Up @@ -109,6 +128,16 @@ export function DepositForm({
</FormMessage>
)
}
{debouncedDepositUsdValue > USD_VALUE_FOR_PRICE_IMPACT_WARNING &&
debouncedPriceImpact > PRICE_IMPACT_THRESHOLD && (
<AlertCard style="warning" title="High Price Impact">
<p>
The price impact of this deposit is{" "}
{formatNumber(debouncedPriceImpact * 100, 2)}%. Deposits with high
price impact may result in lost funds.
</p>
</AlertCard>
)}

<Button
loading={
Expand All @@ -120,7 +149,7 @@ export function DepositForm({
className="w-full mt-2"
disabled={!amount0 && !amount1}
>
Deposit
Deposit ${formatNumber(debouncedDepositUsdValue, 2)}
</Button>
</Form>
);
Expand Down
17 changes: 17 additions & 0 deletions apps/cow-amm-deployer/src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay: number) {
const [debouncedValue, setDebouncedValue] = useState<T>(value);

useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);

return debouncedValue;
}
6 changes: 6 additions & 0 deletions apps/cow-amm-deployer/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export const BLEU_APP_DATA =
"0x4d821ddc9d656177dad4d5c2f76a4bff2ed514ff69fa4aa4fd869d6e98d55c89";

export const PRICE_IMPACT_THRESHOLD = 0.05;

export const UNBALANCED_USD_DIFF_THRESHOLD = 5000;

export const USD_VALUE_FOR_PRICE_IMPACT_WARNING = 5000;
24 changes: 24 additions & 0 deletions apps/cow-amm-deployer/src/lib/priceImpact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export function calculatePriceImpact({
balance0,
balance1,
amount0,
amount1,
}: {
balance0: number;
balance1: number;
amount0: number;
amount1: number;
}) {
const token0Ratio = amount0 / balance0;
const token1Ratio = amount1 / balance1;
const ratio0BiggerThan1 = token0Ratio > token1Ratio;

const currentSpotPrice = ratio0BiggerThan1
? balance1 / balance0
: balance0 / balance1;
const newSpotPrice = ratio0BiggerThan1
? (balance1 + amount1) / (balance0 + amount0)
: (balance0 + amount0) / (balance1 + amount1);

return Math.abs(newSpotPrice - currentSpotPrice) / currentSpotPrice;
}
Loading