Skip to content

Commit

Permalink
Merge pull request #8 from balancer/8020-support
Browse files Browse the repository at this point in the history
Add support for 80/20 in pool creation flow
  • Loading branch information
MattPereira authored Sep 26, 2024
2 parents 0efee2b + 0e86711 commit 0daa8d3
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 17 deletions.
25 changes: 19 additions & 6 deletions packages/nextjs/app/cow/_components/PoolConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { useAccount } from "wagmi";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { Alert, TransactionButton } from "~~/components/common";
import { TextField, TokenField } from "~~/components/common/";
import { ButtonTabs } from "~~/components/common/ButtonTabs";
import { useCheckIfPoolExists } from "~~/hooks/cow";
import { getPoolUrl } from "~~/hooks/cow/getPoolUrl";
import { usePoolCreationPersistedState } from "~~/hooks/cow/usePoolCreationState";
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
import { type Token, useFetchTokenList, useReadToken } from "~~/hooks/token";
import { COW_MIN_AMOUNT } from "~~/utils";
import { SupportedTokenWeight, TokenWeightSelectItems, getPerTokenWeights } from "~~/utils/token-weights";

export const PoolConfiguration = () => {
const { targetNetwork } = useTargetNetwork();
Expand All @@ -25,6 +27,8 @@ export const PoolConfiguration = () => {
const [poolName, setPoolName] = useState<string>("");
const [poolSymbol, setPoolSymbol] = useState<string>("");
const setPersistedState = usePoolCreationPersistedState(state => state.setPersistedState);
const [tokenWeights, setTokenWeights] = useState<SupportedTokenWeight>("5050");
const { token1Weight, token2Weight } = getPerTokenWeights(tokenWeights);

const { data } = useFetchTokenList();
const tokenList = data || [];
Expand Down Expand Up @@ -52,13 +56,14 @@ export const PoolConfiguration = () => {
// Autofill pool name and symbol based on selected tokens
useEffect(() => {
if (token1 !== null && token2 !== null) {
setPoolName(`Balancer CoW AMM 50 ${token1.symbol} 50 ${token2.symbol}`);
setPoolSymbol(`BCoW-50${token1.symbol}-50${token2.symbol}`);
setPoolName(`Balancer CoW AMM ${token1Weight} ${token1.symbol} ${token2Weight} ${token2.symbol}`);
setPoolSymbol(`BCoW-${token1Weight}${token1.symbol}-${token2Weight}${token2.symbol}`);
} else {
setPoolName("");
setPoolSymbol("");
}
}, [token1, token2]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token1, token2, tokenWeights]);

const token1RawAmount = parseUnits(token1Amount, token1?.decimals ?? 0);
const token2RawAmount = parseUnits(token2Amount, token2?.decimals ?? 0);
Expand Down Expand Up @@ -88,6 +93,11 @@ export const PoolConfiguration = () => {
<div className="flex flex-col items-center gap-5 w-full">
<h5 className="text-xl md:text-2xl font-bold">Configure your pool</h5>

<div className="w-full">
<div className="ml-1 mb-1">Select token weights:</div>
<ButtonTabs items={TokenWeightSelectItems} selectedId={tokenWeights} onSelect={setTokenWeights} />
</div>

<div className="w-full">
<div className="ml-1 mb-1">Select pool tokens:</div>
<div className="w-full flex flex-col gap-3">
Expand All @@ -104,6 +114,7 @@ export const PoolConfiguration = () => {
}}
setTokenAmount={setToken1Amount}
tokenOptions={availableTokens || []}
tokenWeight={token1Weight}
/>
<TokenField
value={token2Amount}
Expand All @@ -118,19 +129,20 @@ export const PoolConfiguration = () => {
}}
setTokenAmount={setToken2Amount}
tokenOptions={availableTokens || []}
tokenWeight={token2Weight}
/>
</div>
</div>

<TextField
label="Pool name:"
placeholder="i.e. Balancer CoW AMM 50 BAL 50 DAI"
placeholder={`i.e. Balancer CoW AMM ${token1Weight} BAL ${token2Weight} DAI`}
value={poolName}
onChange={e => setPoolName(e.target.value)}
/>
<TextField
label="Pool symbol:"
placeholder="i.e. BCoW-50BAL-50DAI"
placeholder={`i.e. BCoW-${token1Weight}BAL-${token2Weight}DAI`}
value={poolSymbol}
onChange={e => setPoolSymbol(e.target.value)}
/>
Expand All @@ -151,6 +163,7 @@ export const PoolConfiguration = () => {
poolName: poolName.trim(),
poolSymbol: poolSymbol.trim(),
step: 1,
tokenWeights,
});
}}
/>
Expand Down Expand Up @@ -184,7 +197,7 @@ export const PoolConfiguration = () => {
checked={hasAgreedToWarning}
/>
<span className="">
I understand that assets must be added proportionally, or I risk loss of funds via arbitrage.
I understand that assets must be added proportional to the selected token weights.
</span>
</label>
</div>
Expand Down
21 changes: 16 additions & 5 deletions packages/nextjs/app/cow/_components/PoolCreation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { useApproveToken, useReadToken } from "~~/hooks/token";
import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
import { getPerTokenWeights } from "~~/utils/token-weights";

interface ManagePoolCreationProps {
state: PoolCreationState;
Expand All @@ -26,6 +27,7 @@ interface ManagePoolCreationProps {
export const PoolCreation = ({ state, clearState }: ManagePoolCreationProps) => {
const token1RawAmount = parseUnits(state.token1Amount, state.token1.decimals);
const token2RawAmount = parseUnits(state.token2Amount, state.token2.decimals);
const { token1Weight, token2Weight } = getPerTokenWeights(state.tokenWeights);

const [userPoolAddress, setUserPoolAddress] = useState<Address>();
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
Expand All @@ -43,12 +45,11 @@ export const PoolCreation = ({ state, clearState }: ManagePoolCreationProps) =>
state.token2.address,
pool?.address,
);

const { mutate: createPool, isPending: isCreatePending, error: createPoolError } = useCreatePool();
const { mutate: approve1, isPending: isApprove1Pending, error: approve1Error } = useApproveToken();
const { mutate: approve2, isPending: isApprove2Pending, error: approve2Error } = useApproveToken();
const { mutate: bind1, isPending: isBind1Pending, error: bind1Error } = useBindToken();
const { mutate: bind2, isPending: isBind2Pending, error: bind2Error } = useBindToken();
const { mutate: bind1, isPending: isBind1Pending, error: bind1Error } = useBindToken(state.tokenWeights, true);
const { mutate: bind2, isPending: isBind2Pending, error: bind2Error } = useBindToken(state.tokenWeights, false);
const { mutate: setSwapFee, isPending: isSetSwapFeePending, error: setSwapFeeError } = useSetSwapFee();
const { mutate: finalizePool, isPending: isFinalizePending, error: finalizeError } = useFinalizePool();
const txError =
Expand Down Expand Up @@ -90,8 +91,18 @@ export const PoolCreation = ({ state, clearState }: ManagePoolCreationProps) =>
<div className="w-full">
<div className="ml-1 mb-1">Selected pool tokens:</div>
<div className="w-full flex flex-col gap-3">
<TokenField value={state.token1Amount} selectedToken={state.token1} isDisabled={true} />
<TokenField value={state.token2Amount} selectedToken={state.token2} isDisabled={true} />
<TokenField
value={state.token1Amount}
selectedToken={state.token1}
isDisabled={true}
tokenWeight={token1Weight}
/>
<TokenField
value={state.token2Amount}
selectedToken={state.token2}
isDisabled={true}
tokenWeight={token2Weight}
/>
</div>
</div>
<TextField label="Pool name:" value={state.poolName} isDisabled={true} />
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 @@ -7,7 +7,7 @@ const PAGES = [
emoji: <CowLogo />,
title: "CoW AMMs",
href: "/cow",
description: "Deploy pools with 2 tokens and 50/50 weight distribution",
description: "Deploy a CoW AMM pool",
},
];

Expand Down
43 changes: 43 additions & 0 deletions packages/nextjs/components/common/ButtonTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";
import { SupportedTokenWeight } from "~~/utils/token-weights";

interface Props {
items: {
label: string;
id: SupportedTokenWeight;
}[];
selectedId: SupportedTokenWeight;
onSelect: (id: SupportedTokenWeight) => void;
}

export const ButtonTabs: React.FC<Props> = ({ items, selectedId, onSelect }) => {
return (
<div className="w-full flex flex-col gap-3">
<div className="tabs">
<div className="flex">
<div className="flex flex-1 rounded-xl transition-all duration-300 -mb-px overflow-hidden">
{items.map(({ id, label }) => {
const isSelected = id === selectedId;

return (
<button
key={id}
className={`text-neutral-700 bg-gradient-to-b from-custom-beige-start to-custom-beige-end to-100% flex-1 py-3 font-medium text-lg front-bold ${
isSelected ? "" : "opacity-50 hover:opacity-90"
}`}
onClick={() => {
if (selectedId !== id) {
onSelect(id);
}
}}
>
{label}
</button>
);
})}
</div>
</div>
</div>
</div>
);
};
6 changes: 5 additions & 1 deletion packages/nextjs/components/common/TokenField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface TokenFieldProps {
tokenOptions?: Token[];
setToken?: (token: Token) => void;
setTokenAmount?: (amount: string) => void;
tokenWeight: string;
}

export const TokenField: React.FC<TokenFieldProps> = ({
Expand All @@ -29,6 +30,7 @@ export const TokenField: React.FC<TokenFieldProps> = ({
tokenOptions,
setToken,
setTokenAmount,
tokenWeight,
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const { data: tokenPrices, isLoading, isError } = useFetchTokenPrices();
Expand Down Expand Up @@ -80,7 +82,9 @@ export const TokenField: React.FC<TokenFieldProps> = ({
} px-3 py-1.5 shadow-md disabled:text-base-content text-lg font-bold disabled:bg-base-100 rounded-lg flex justify-between items-center gap-2 mb-[1px]`}
>
{selectedToken && <TokenImage size="sm" token={selectedToken} />}
{selectedToken?.symbol ? selectedToken.symbol : "Select Token"}{" "}
{selectedToken?.symbol
? `${selectedToken.symbol} ${tokenWeight}%`
: `Select ${tokenWeight !== "50" ? `${tokenWeight}% ` : ""}Token`}{" "}
{!isDisabled && <ChevronDownIcon className="w-4 h-4 mt-0.5" />}
</button>

Expand Down
8 changes: 4 additions & 4 deletions packages/nextjs/hooks/cow/useBindToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { Address } from "viem";
import { usePublicClient, useWalletClient } from "wagmi";
import { abis } from "~~/contracts/abis";
import { useTransactor } from "~~/hooks/scaffold-eth";
import { SupportedTokenWeight, getDenormalizedTokenWeight } from "~~/utils/token-weights";

type BindPayload = {
pool: Address | undefined;
token: Address;
rawAmount: bigint;
};

const DENORMALIZED_WEIGHT = 1000000000000000000n; // bind 2 tokens with 1e18 weight for each to get a 50/50 pool

export const useBindToken = () => {
export const useBindToken = (tokenWeights: SupportedTokenWeight, isToken1: boolean) => {
const { data: walletClient } = useWalletClient();
const publicClient = usePublicClient();
const writeTx = useTransactor(); // scaffold hook for tx status toast notifications
const denormalizedTokenWeight = getDenormalizedTokenWeight(tokenWeights, isToken1);

const bind = async ({ pool, token, rawAmount }: BindPayload) => {
if (!pool) throw new Error("Cannot bind token without pool address");
Expand All @@ -27,7 +27,7 @@ export const useBindToken = () => {
address: pool,
functionName: "bind",
account: walletClient.account,
args: [token, rawAmount, DENORMALIZED_WEIGHT],
args: [token, rawAmount, denormalizedTokenWeight],
});

await writeTx(() => walletClient.writeContract(bind), {
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/hooks/cow/usePoolCreationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface PoolCreationState {
poolName: string;
poolSymbol: string;
step: number;
tokenWeights: "5050" | "8020";
}

export const usePoolCreationPersistedState = create(
Expand Down
29 changes: 29 additions & 0 deletions packages/nextjs/utils/token-weights.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type SupportedTokenWeight = "5050" | "8020";
export interface TokenWeightSelectItem {
id: SupportedTokenWeight;
label: string;
}

export const TokenWeightSelectItems: TokenWeightSelectItem[] = [
{ id: "5050", label: "50/50" },
{ id: "8020", label: "80/20" },
];

export function getPerTokenWeights(tokenWeights: SupportedTokenWeight) {
return {
token1Weight: tokenWeights === "8020" ? "80" : "50",
token2Weight: tokenWeights === "8020" ? "20" : "50",
};
}

export function getDenormalizedTokenWeight(tokenWeights: SupportedTokenWeight, isToken1: boolean) {
if (tokenWeights === "8020") {
if (isToken1) {
return 8000000000000000000n;
} else {
return 2000000000000000000n;
}
}

return 1000000000000000000n; // bind 2 tokens with 1e18 weight for each to get a 50/50 pool
}

0 comments on commit 0daa8d3

Please sign in to comment.