Skip to content

Commit

Permalink
CoW AMM: Price impact information on deposit and creation of AMMs (#703)
Browse files Browse the repository at this point in the history
* Add usd value and price impact warning on deposit form

* add create amm unbalanced amm warning

* refactor: separete thresholds into constants file

* run formatter

* update cowAmmData to ammData and run formatter
  • Loading branch information
yvesfracari authored Jun 13, 2024
1 parent 41cd197 commit e058f5b
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 5 deletions.
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;
}

0 comments on commit e058f5b

Please sign in to comment.