diff --git a/components/trade-form/BuyForm.tsx b/components/trade-form/BuyForm.tsx index 4d99a9dad..b28b7b1da 100644 --- a/components/trade-form/BuyForm.tsx +++ b/components/trade-form/BuyForm.tsx @@ -25,6 +25,7 @@ import { approximateMaxAmountInForBuy, calculateSpotPrice, calculateSwapAmountOutForBuy, + isValidBuyAmount, } from "lib/util/amm2"; import { formatNumberCompact } from "lib/util/format-compact"; import { parseAssetIdString } from "lib/util/parse-asset-id"; @@ -93,6 +94,21 @@ const BuyForm = ({ const assetReserve = pool?.reserves && lookupAssetReserve(pool?.reserves, selectedAsset); + const validBuy = useMemo(() => { + return ( + assetReserve && + pool.liquidity && + swapFee && + isValidBuyAmount( + assetReserve, + amountIn, + pool.liquidity, + swapFee, + creatorFee, + ) + ); + }, [assetReserve, pool?.liquidity, amountIn]); + const maxAmountIn = useMemo(() => { return ( assetReserve && @@ -274,6 +290,8 @@ const BuyForm = ({ return `Maximum amount of ${baseSymbol} that can be traded is ${maxAmountIn .div(ZTG) .toFixed(3)}`; + } else if (validBuy?.isValid === false) { + return validBuy.message; } }, })} diff --git a/components/trade-form/SellForm.tsx b/components/trade-form/SellForm.tsx index 595ceb151..ad3624448 100644 --- a/components/trade-form/SellForm.tsx +++ b/components/trade-form/SellForm.tsx @@ -24,6 +24,7 @@ import { approximateMaxAmountInForSell, calculateSpotPrice, calculateSwapAmountOutForSell, + isValidSellAmount, } from "lib/util/amm2"; import { formatNumberCompact } from "lib/util/format-compact"; import { parseAssetIdString } from "lib/util/parse-asset-id"; @@ -95,6 +96,15 @@ const SellForm = ({ const assetReserve = pool?.reserves && lookupAssetReserve(pool?.reserves, selectedAsset); + const validSell = useMemo(() => { + return ( + assetReserve && + pool.liquidity && + swapFee && + isValidSellAmount(assetReserve, amountIn, pool.liquidity) + ); + }, [assetReserve, pool?.liquidity, amountIn]); + const maxAmountIn = useMemo(() => { return ( assetReserve && @@ -243,6 +253,8 @@ const SellForm = ({ return `Maximum amount that can be traded is ${maxAmountIn .div(ZTG) .toFixed(3)}`; + } else if (validSell?.isValid === false) { + return validSell.message; } }, })} diff --git a/lib/util/amm2.spec.ts b/lib/util/amm2.spec.ts index 7919ac70b..3e6704f6f 100644 --- a/lib/util/amm2.spec.ts +++ b/lib/util/amm2.spec.ts @@ -6,7 +6,11 @@ import { calculateSwapAmountOutForBuy, calculateSwapAmountOutForSell, calculatePoolAmounts, + isValidBuyAmount, + isValidSellAmount, + calculateReserveAfterSell, } from "./amm2"; +import { ZTG } from "@zeitgeistpm/sdk"; // test cases copied from https://github.com/zeitgeistpm/zeitgeist/blob/f0586d32c692f738b04d03bec4e59a73d6899182/zrml/neo-swaps/src/math.rs describe("amm2", () => { @@ -26,7 +30,7 @@ describe("amm2", () => { test("should work with fees", () => { const amountOut = calculateSwapAmountOutForBuy( new Decimal(1_000_000_000_000), - new Decimal(109270000000), + new Decimal(109_270_000_000), new Decimal(1_442_695_040_889), new Decimal(0.03), new Decimal(0.005), @@ -147,6 +151,94 @@ describe("amm2", () => { }); }); + describe("isValidBuyAmount", () => { + test("should return true if amount in is allowed", () => { + const { isValid, message } = isValidBuyAmount( + new Decimal(10 * 10 ** 10), + new Decimal(10 * 10 ** 10), + new Decimal(144269504088), + new Decimal(0), + new Decimal(0), + ); + + expect(isValid).toEqual(true); + expect(message).toEqual(undefined); + }); + + test("should return false if amount in is too high", () => { + const { isValid, message } = isValidBuyAmount( + new Decimal(10 * 10 ** 10), + new Decimal(10000 * 10 ** 10), + new Decimal(144269504088), + new Decimal(0), + new Decimal(0), + ); + + expect(isValid).toEqual(false); + expect(message).toEqual("Amount in too high"); + }); + + test("should return false if amount in is too low", () => { + const { isValid, message } = isValidBuyAmount( + new Decimal(100 * 10 ** 10), + new Decimal(1 * 10 ** 10), + new Decimal(144269504088), + new Decimal(0), + new Decimal(0), + ); + + expect(isValid).toEqual(false); + expect(message).toEqual("Amount in too low"); + }); + }); + + describe("isValidSellAmount", () => { + test("should return true if amount in is allowed", () => { + const { isValid, message } = isValidSellAmount( + new Decimal(10 * 10 ** 10), + new Decimal(10 * 10 ** 10), + new Decimal(144269504088), + ); + + expect(isValid).toEqual(true); + expect(message).toEqual(undefined); + }); + + test("should return false if amount in is too high ", () => { + const { isValid, message } = isValidSellAmount( + new Decimal(10 * 10 ** 10), + new Decimal(10000 * 10 ** 10), + new Decimal(144269504088), + ); + + expect(isValid).toEqual(false); + expect(message).toEqual("Amount in too high"); + }); + + test("should return false if price is too low", () => { + const { isValid, message } = isValidSellAmount( + new Decimal(1000 * 10 ** 10), + new Decimal(10 * 10 ** 10), + new Decimal(144269504088), + ); + + expect(isValid).toEqual(false); + expect(message).toEqual("Price is low to sell"); + }); + }); + + describe("calculateReserveAfterSell", () => { + test("should work", () => { + const newReserve = calculateReserveAfterSell( + new Decimal(10 * 10 ** 10), + new Decimal(10 * 10 ** 10), + new Decimal(144269504088), + ); + + expect(newReserve.div(ZTG).toFixed(3)).toEqual("15.850"); + }); + }); + test("all functions", () => { const liquidity = new Decimal(144.00003701590745); const reserves = [ diff --git a/lib/util/amm2.ts b/lib/util/amm2.ts index 59f832a3b..bc0589eda 100644 --- a/lib/util/amm2.ts +++ b/lib/util/amm2.ts @@ -119,3 +119,86 @@ export const calculatePoolAmounts = ( return poolAmounts; }; + +export const isValidBuyAmount = ( + assetReserve: Decimal, + amountIn: Decimal, + liquidityParameter: Decimal, + poolFee: Decimal, // 1% is 0.01 + creatorFee: Decimal, // 1% is 0.01 +) => { + const totalFee = poolFee.plus(creatorFee); + const feeMultiplier = new Decimal(1).minus(totalFee); + const amountInMinusFees = amountIn.mul(feeMultiplier); + + if ( + amountInMinusFees.greaterThanOrEqualTo( + calculatePoolNumericalThreshold(liquidityParameter), + ) + ) { + return { isValid: false, message: "Amount in too high" }; + } else if ( + calculateBuyLnArgument( + assetReserve, + amountInMinusFees, + liquidityParameter, + ).lessThanOrEqualTo(lsmrConstant) + ) { + return { isValid: false, message: "Amount in too low" }; + } else { + return { isValid: true }; + } +}; + +export const isValidSellAmount = ( + assetReserve: Decimal, + amountIn: Decimal, + liquidityParameter: Decimal, +) => { + const numericalThreshold = + calculatePoolNumericalThreshold(liquidityParameter); + if (assetReserve.greaterThanOrEqualTo(numericalThreshold)) { + return { isValid: false, message: "Price is low to sell" }; + } else if (amountIn.greaterThanOrEqualTo(numericalThreshold)) { + return { isValid: false, message: "Amount in too high" }; + } else if ( + amountIn.greaterThanOrEqualTo(numericalThreshold) || + calculateReserveAfterSell( + assetReserve, + amountIn, + liquidityParameter, + ).greaterThanOrEqualTo(numericalThreshold) + ) { + return { isValid: false, message: "Amount in too high" }; + } else { + return { isValid: true }; + } +}; + +export const calculateReserveAfterSell = ( + assetReserve: Decimal, + amountIn: Decimal, + liquidity: Decimal, // liqudity parameter of the pool +) => { + // new_reserve = old_reserve + b ln(exp(old_reserve/liquidity_param) - 1 + exp(-amount/liquidity_param)) + const term1 = assetReserve.div(liquidity).exp(); + const term2 = new Decimal(0).minus(amountIn).div(liquidity).exp(); + + return term1.plus(term2).minus(1).ln().mul(liquidity).plus(assetReserve); +}; + +const lsmrConstant = 0.1; + +const calculatePoolNumericalThreshold = (liquidityParameter: Decimal) => + liquidityParameter.mul(10); + +const calculateBuyLnArgument = ( + assetReserve: Decimal, + amountInMinusFee: Decimal, + liquidity: Decimal, // liqudity parameter of the pool +) => { + const term1 = amountInMinusFee.div(liquidity).exp(); + const term2 = new Decimal(0).minus(assetReserve).div(liquidity).exp(); + + return term1.plus(term2).minus(1); +};