Skip to content

Commit

Permalink
Fix/fractional order form rounding errors (#317)
Browse files Browse the repository at this point in the history
* fix: prevent rounding errors in unit calculations by using DEFAULT_NUM_FRACTIONS precision

* fix: disable form when calculated number of units reaches 0, show price 0

* fix: calculate total price using units not percentage in fractional order form

* fix: remove magic number for number of decimals in hypercert units

* fix: remove redundant utility function getTotalPriceFromPercentage
  • Loading branch information
Jipperism authored Nov 26, 2024
1 parent f9ab44e commit d7f35e9
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 63 deletions.
36 changes: 22 additions & 14 deletions components/marketplace/buy-fractional-order-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ import {
getCurrencyByAddress,
getPricePerPercent,
getPricePerUnit,
getTotalPriceFromPercentage,
} from "@/marketplace/utils";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { parseUnits } from "viem";
import { calculateBigIntPercentage } from "@/lib/calculateBigIntPercentage";
import {
DEFAULT_NUM_FRACTIONS,
DEFAULT_NUM_FRACTIONS_DECIMALS,
} from "@/configs/hypercerts";

const formSchema = z
.object({
Expand Down Expand Up @@ -83,13 +86,17 @@ export const BuyFractionalOrderForm = ({
const getUnitsToBuy = (percentageAmount: string) => {
try {
const hypercertUnits = BigInt(hypercert.units || 0);
const percentageAsBigInt = BigInt(Number(percentageAmount) * 100);
const percentageAsBigInt = parseUnits(
percentageAmount,
DEFAULT_NUM_FRACTIONS_DECIMALS,
);
const unitsToBuy =
(hypercertUnits * percentageAsBigInt) / BigInt(100 * 100);
return unitsToBuy.toString();
(hypercertUnits * percentageAsBigInt) /
(BigInt(100) * DEFAULT_NUM_FRACTIONS);
return unitsToBuy < BigInt(0) ? BigInt(0) : unitsToBuy;
} catch (e) {
console.error(e);
return "0";
return BigInt(0);
}
};

Expand Down Expand Up @@ -153,12 +160,15 @@ export const BuyFractionalOrderForm = ({
const percentageAmount = form.watch("percentageAmount");
const pricePerPercent = form.watch("pricePerPercent");

const unitsToBuy = getUnitsToBuy(percentageAmount);
const pricePerUnit = getPricePerUnit(
pricePerPercent,
BigInt(hypercert.units || 0),
);

const totalPrice = formatPrice(
order.chainId,
getTotalPriceFromPercentage(
BigInt(pricePerPercent),
Number(percentageAmount),
),
unitsToBuy * pricePerUnit,
currency.address,
true,
);
Expand All @@ -169,10 +179,7 @@ export const BuyFractionalOrderForm = ({
currency.address,
);

const unitsToBuy =
BigInt(getUnitsToBuy(percentageAmount)) > BigInt(0)
? getUnitsToBuy(percentageAmount)
: "0";
const disabled = !form.formState.isValid || unitsToBuy === BigInt(0);

return (
<Form {...form}>
Expand All @@ -189,7 +196,7 @@ export const BuyFractionalOrderForm = ({
<div className="text-sm text-slate-500">
You will buy{" "}
<b>
<FormattedUnits>{unitsToBuy}</FormattedUnits>
<FormattedUnits>{unitsToBuy.toString()}</FormattedUnits>
</b>{" "}
units , for a total of <b>{totalPrice}</b>. (min:{" "}
{minPercentageAmount}%, max: {maxPercentageAmount}%)
Expand Down Expand Up @@ -238,6 +245,7 @@ export const BuyFractionalOrderForm = ({
variant={"outline"}
type="button"
onClick={form.handleSubmit(onSubmit)}
disabled={disabled}
>
Execute order
</Button>
Expand Down
6 changes: 5 additions & 1 deletion configs/hypercerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ const HYPERCERT_API_URL =
export const HYPERCERTS_API_URL_REST = `${HYPERCERT_API_URL}/v1`;
export const HYPERCERTS_API_URL_GRAPH = `${HYPERCERT_API_URL}/v1/graphql`;

export const DEFAULT_NUM_FRACTIONS = parseUnits("1", 8);
export const DEFAULT_NUM_FRACTIONS_DECIMALS = 8;
export const DEFAULT_NUM_FRACTIONS = parseUnits(
"1",
DEFAULT_NUM_FRACTIONS_DECIMALS,
);

export const DEFAULT_DISPLAY_CURRENCY = "usd";
12 changes: 4 additions & 8 deletions marketplace/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ export const useBuyFractionalMakerAsk = () => {
totalUnitsInHypercert,
}: {
order: MarketplaceOrder;
unitAmount: string;
unitAmount: bigint;
pricePerUnit: string;
hypercertName?: string | null;
totalUnitsInHypercert?: bigint;
Expand Down Expand Up @@ -473,7 +473,7 @@ export const useBuyFractionalMakerAsk = () => {
takerOrder = hypercertExchangeClient.createFractionalSaleTakerBid(
order,
address,
unitAmount,
unitAmount.toString(),
pricePerUnit,
);
} catch (e) {
Expand All @@ -492,7 +492,7 @@ export const useBuyFractionalMakerAsk = () => {
);
}

const totalPrice = BigInt(order.price) * BigInt(unitAmount);
const totalPrice = BigInt(order.price) * unitAmount;
try {
await setStep("ERC20");
if (currency.address !== zeroAddress) {
Expand Down Expand Up @@ -566,11 +566,7 @@ export const useBuyFractionalMakerAsk = () => {
<span>
Congratulations, you successfully bought{" "}
<b>
{calculateBigIntPercentage(
BigInt(unitAmount),
totalUnitsInHypercert,
)}
%
{calculateBigIntPercentage(unitAmount, totalUnitsInHypercert)}%
</b>{" "}
of <b>{hypercertName}</b>.
</span>
Expand Down
16 changes: 0 additions & 16 deletions marketplace/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,3 @@ export const isTokenDividableBy = (
(parseUnits(denominator || "1", currency.decimals) || BigInt(1));
return remainder === BigInt(0);
};

export const getTotalPriceFromPercentage = (
pricePerPercent: bigint,
percentageAmount: number,
) => {
if (percentageAmount < 0 || percentageAmount > 100) {
throw new Error("Percentage amount must be between 0 and 100");
}

const precision = 10 ** 16;

return (
(pricePerPercent * BigInt(Math.round(percentageAmount * precision))) /
BigInt(precision)
);
};
25 changes: 1 addition & 24 deletions test/marketplace/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { describe, it, expect } from "vitest";

import {
getPricePerPercent,
getPricePerUnit,
getTotalPriceFromPercentage,
} from "@/marketplace/utils";
import { getPricePerPercent, getPricePerUnit } from "@/marketplace/utils";
import { currenciesByNetwork } from "@hypercerts-org/marketplace-sdk";
import { sepolia } from "viem/chains";

Expand Down Expand Up @@ -42,23 +38,4 @@ describe("utils", () => {
// expect(getMinimumPrice("1", chainId, usdc.address)).to.eq(1n);
});
});

describe("getTotalPriceFromPercentage", () => {
it("should return the total price from percentage", () => {
expect(getTotalPriceFromPercentage(BigInt(1), 100)).to.eq(BigInt(100));
expect(() => getTotalPriceFromPercentage(BigInt(1), 200)).toThrowError();

expect(getTotalPriceFromPercentage(BigInt(1), 10)).to.eq(BigInt(10));
expect(getTotalPriceFromPercentage(BigInt(100), 0.1)).to.eq(BigInt(10));
expect(getTotalPriceFromPercentage(BigInt(10 ** 6), 0.00001)).to.eq(
BigInt(10),
);
expect(
getTotalPriceFromPercentage(BigInt(10 ** 12), 0.00000000001),
).to.eq(BigInt(10));
expect(
getTotalPriceFromPercentage(BigInt(10 ** 16), 0.000000000000001),
).to.eq(BigInt(10));
});
});
});

0 comments on commit d7f35e9

Please sign in to comment.