Skip to content

Commit 4188199

Browse files
Merge pull request #101 from rohans02/feat/coin-decimal-support
Make frontend decimal-aware for base tokens
2 parents 83e771d + edc248a commit 4188199

3 files changed

Lines changed: 85 additions & 41 deletions

File tree

src/app/[pool]/InteractionClient.tsx

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const usePool = (poolId: Address | undefined, isConnected: boolean) => {
5858
treasury_fee: number;
5959
bull_percentage: number;
6060
bear_percentage: number;
61+
base_decimals: number;
62+
base_symbol: string;
6163
chainId: number;
6264
} | null>(null);
6365
const [loading, setLoading] = useState(true);
@@ -94,6 +96,8 @@ const usePool = (poolId: Address | undefined, isConnected: boolean) => {
9496
{ address: bearAddr, abi: CoinABI, functionName: 'totalSupply' },
9597
{ address: baseToken, abi: ERC20ABI, functionName: 'balanceOf', args: [bullAddr] },
9698
{ address: baseToken, abi: ERC20ABI, functionName: 'balanceOf', args: [bearAddr] },
99+
{ address: baseToken, abi: ERC20ABI, functionName: 'decimals' },
100+
{ address: baseToken, abi: ERC20ABI, functionName: 'symbol' },
97101
] : [],
98102
query: {
99103
enabled: !!(bullAddr && bearAddr),
@@ -151,9 +155,12 @@ const usePool = (poolId: Address | undefined, isConnected: boolean) => {
151155
const bearSupply = tokenData?.[5]?.result as bigint || BigInt(0);
152156
const bullReserve = tokenData?.[6]?.result as bigint || BigInt(0);
153157
const bearReserve = tokenData?.[7]?.result as bigint || BigInt(0);
158+
// Reserves are base-token balances, denominated in base-token decimals (not 18).
159+
const baseDecimals = tokenData?.[8]?.result !== undefined ? Number(tokenData[8].result) : 18;
160+
const baseSymbol = tokenData?.[9]?.result as string || 'tokens';
154161

155-
const totalReserves = Number(formatUnits(bullReserve, 18)) + Number(formatUnits(bearReserve, 18));
156-
const bullPercentage = totalReserves > 0 ? (Number(formatUnits(bullReserve, 18)) / totalReserves) * 100 : 50;
162+
const totalReserves = Number(formatUnits(bullReserve, baseDecimals)) + Number(formatUnits(bearReserve, baseDecimals));
163+
const bullPercentage = totalReserves > 0 ? (Number(formatUnits(bullReserve, baseDecimals)) / totalReserves) * 100 : 50;
157164
const bearPercentage = 100 - bullPercentage;
158165

159166
// const userBullBalance = userBalancesData?.[0]?.result as bigint || BigInt(0);
@@ -190,6 +197,8 @@ const usePool = (poolId: Address | undefined, isConnected: boolean) => {
190197
treasury_fee: poolFeeData?.[3]?.result ? Number(poolFeeData[3].result) / 1000 : 0,
191198
bull_percentage: bullPercentage,
192199
bear_percentage: bearPercentage,
200+
base_decimals: baseDecimals,
201+
base_symbol: baseSymbol,
193202
chainId: chain?.id || 11155111,
194203
};
195204

@@ -211,12 +220,12 @@ const usePool = (poolId: Address | undefined, isConnected: boolean) => {
211220
};
212221

213222

214-
const formatValue = (value: number) => `${formatNumber(value, 3)} WETH`;
223+
const formatValue = (value: number, symbol: string) => `${formatNumber(value, 3)} ${symbol}`;
215224

216225
// Timeout duration for stuck transactions (5 minutes)
217226
const TX_TIMEOUT_MS = 5 * 60 * 1000;
218227

219-
function VaultSection({ isBull, poolData, userTokens, price, value, symbol, connected, handlePoll, reserve, supply, tokenAddress }: {
228+
function VaultSection({ isBull, poolData, userTokens, price, value, symbol, connected, handlePoll, reserve, supply, tokenAddress, baseDecimals, baseSymbol }: {
220229
isBull: boolean;
221230
poolData: {
222231
id: { id: string };
@@ -246,6 +255,8 @@ function VaultSection({ isBull, poolData, userTokens, price, value, symbol, conn
246255
reserve: number;
247256
supply: number;
248257
tokenAddress: string;
258+
baseDecimals: number;
259+
baseSymbol: string;
249260
}) {
250261
const { address } = useAccount();
251262
const { writeContractAsync, data: hash, isPending: isTransactionPending } = useWriteContract();
@@ -334,7 +345,7 @@ function VaultSection({ isBull, poolData, userTokens, price, value, symbol, conn
334345

335346
const handleBuyTransaction = useCallback(async (amount: string) => {
336347
try {
337-
const amountWei = parseUnits(amount, 18);
348+
const amountWei = parseUnits(amount, baseDecimals);
338349

339350
setPendingTransactionType('buy');
340351

@@ -379,7 +390,7 @@ function VaultSection({ isBull, poolData, userTokens, price, value, symbol, conn
379390
}
380391
pendingTransactionToastIdRef.current = null;
381392
}
382-
}, [tokenAddress, address, writeContractAsync]);
393+
}, [tokenAddress, address, writeContractAsync, baseDecimals]);
383394

384395
const handleBuy = withErrorHandling(async () => {
385396
if (!address || !connected) {
@@ -409,12 +420,12 @@ function VaultSection({ isBull, poolData, userTokens, price, value, symbol, conn
409420
throw createTransactionError(errorMessage);
410421
}
411422

412-
const amountWei = parseUnits(validatedInput.amount.toString(), 18);
423+
const amountWei = parseUnits(validatedInput.amount.toString(), baseDecimals);
413424

414425
// Check user's base token balance
415426
const userBaseTokenBalance = baseTokenBalance || BigInt(0);
416427
if (userBaseTokenBalance < amountWei) {
417-
const errorMessage = `Insufficient balance. You have ${formatUnits(userBaseTokenBalance, 18)} base tokens available.`;
428+
const errorMessage = `Insufficient balance. You have ${formatUnits(userBaseTokenBalance, baseDecimals)} ${baseSymbol} available.`;
418429
toast.error(errorMessage);
419430
throw createTransactionError(errorMessage);
420431
}
@@ -619,15 +630,15 @@ function VaultSection({ isBull, poolData, userTokens, price, value, symbol, conn
619630
<div className="space-y-2 mb-4">
620631
<div className="flex justify-between text-sm">
621632
<span className="text-gray-600 dark:text-gray-400">Reserve</span>
622-
<span className="font-medium text-black dark:text-white">{formatNumber(reserve, 6)} WETH</span>
633+
<span className="font-medium text-black dark:text-white">{formatNumber(reserve, 6)} {baseSymbol}</span>
623634
</div>
624635
<div className="flex justify-between text-sm">
625636
<span className="text-gray-600 dark:text-gray-400">Supply</span>
626637
<span className="font-medium text-black dark:text-white">{formatNumber(supply, 6)} {symbol}</span>
627638
</div>
628639
<div className="flex justify-between text-sm">
629640
<span className="text-gray-600 dark:text-gray-400">Price</span>
630-
<span className="font-medium text-black dark:text-white">{formatNumber(price, 6)} WETH</span>
641+
<span className="font-medium text-black dark:text-white">{formatNumber(price, 6)} {baseSymbol}</span>
631642
</div>
632643
</div>
633644

@@ -652,7 +663,7 @@ function VaultSection({ isBull, poolData, userTokens, price, value, symbol, conn
652663
</div>
653664
<div className="flex justify-between text-sm">
654665
<span className="text-gray-600 dark:text-gray-400">Value</span>
655-
<span className="font-medium text-black dark:text-white">{formatNumber(value, 4)} WETH</span>
666+
<span className="font-medium text-black dark:text-white">{formatNumber(value, 4)} {baseSymbol}</span>
656667
</div>
657668
</div>
658669
</div>
@@ -664,18 +675,19 @@ function VaultSection({ isBull, poolData, userTokens, price, value, symbol, conn
664675
<div>
665676
<Input
666677
type="number"
667-
placeholder="Enter WETH amount"
678+
placeholder={`Enter ${baseSymbol} amount`}
668679
value={buyAmount}
669680
onChange={(e) => setBuyAmount(e.target.value)}
670681
className="w-full"
671682
disabled={isTransacting}
672683
/>
673-
<div
674-
className="mt-1 text-xs text-gray-500 dark:text-gray-400 cursor-pointer"
675-
onClick={() => setBuyAmount(formatNumberDown(Number(formatUnits(baseTokenBalance, 18)), 4))}
684+
<button
685+
type="button"
686+
className="mt-1 text-xs text-gray-500 dark:text-gray-400 cursor-pointer bg-transparent border-none p-0 text-left"
687+
onClick={() => setBuyAmount(formatNumberDown(Number(formatUnits(baseTokenBalance, baseDecimals)), 4))}
676688
>
677-
Max: {formatNumberDown(Number(formatUnits(baseTokenBalance, 18)), 4)} WETH
678-
</div>
689+
Max: {formatNumberDown(Number(formatUnits(baseTokenBalance, baseDecimals)), 4)} {baseSymbol}
690+
</button>
679691
</div>
680692
<Button
681693
onClick={() => handleBuy()}
@@ -700,12 +712,13 @@ function VaultSection({ isBull, poolData, userTokens, price, value, symbol, conn
700712
className="w-full"
701713
disabled={isTransacting}
702714
/>
703-
<div
704-
className="mt-1 text-xs text-gray-500 dark:text-gray-400 cursor-pointer"
715+
<button
716+
type="button"
717+
className="mt-1 text-xs text-gray-500 dark:text-gray-400 cursor-pointer bg-transparent border-none p-0 text-left"
705718
onClick={() => setSellAmount(formatNumberDown(Number(formatUnits(userTokens, 18)), 4))}
706719
>
707720
Max: {formatNumberDown(Number(formatUnits(userTokens, 18)), 4)} {symbol}
708-
</div>
721+
</button>
709722
</div>
710723
<Button
711724
onClick={() => handleSell()}
@@ -1214,6 +1227,8 @@ export default function InteractionClient() {
12141227
treasury_fee: pool.treasury_fee || 0,
12151228
bull_percentage: pool.bull_percentage || 50,
12161229
bear_percentage: pool.bear_percentage || 50,
1230+
base_decimals: pool.base_decimals ?? 18,
1231+
base_symbol: pool.base_symbol || 'tokens',
12171232
chainId: pool.chainId || 1,
12181233
}
12191234
: {
@@ -1233,12 +1248,14 @@ export default function InteractionClient() {
12331248
treasury_fee: 0,
12341249
bull_percentage: 50,
12351250
bear_percentage: 50,
1251+
base_decimals: 18,
1252+
base_symbol: 'tokens',
12361253
chainId: 1,
12371254
}, [pool]);
12381255

12391256
const calculations = useMemo(() => {
1240-
const bullReserveNum = Number(formatUnits(poolData.bull_reserve, 18));
1241-
const bearReserveNum = Number(formatUnits(poolData.bear_reserve, 18));
1257+
const bullReserveNum = Number(formatUnits(poolData.bull_reserve, poolData.base_decimals));
1258+
const bearReserveNum = Number(formatUnits(poolData.bear_reserve, poolData.base_decimals));
12421259
const bullSupplyNum = Number(formatUnits(poolData.bull_token.fields.total_supply, 18));
12431260
const bearSupplyNum = Number(formatUnits(poolData.bear_token.fields.total_supply, 18));
12441261
const userBullTokens = Number(formatUnits(userBalances.bull_tokens, 18));
@@ -1476,7 +1493,7 @@ export default function InteractionClient() {
14761493
Total Value Locked
14771494
</div>
14781495
<div className="text-sm md:text-lg font-bold transition-all duration-300">
1479-
{formatValue(calculations.totalReserves)}
1496+
{formatValue(calculations.totalReserves, poolData.base_symbol)}
14801497
</div>
14811498
<div className="w-full rounded-full h-2 my-2 flex overflow-hidden bg-neutral-200 dark:bg-neutral-700">
14821499
<div
@@ -1522,6 +1539,8 @@ export default function InteractionClient() {
15221539
reserve={calculations.bullReserveNum}
15231540
supply={calculations.bullSupplyNum}
15241541
tokenAddress={poolData.bull_token.id}
1542+
baseDecimals={poolData.base_decimals}
1543+
baseSymbol={poolData.base_symbol}
15251544
/>
15261545

15271546
<div className="lg:col-span-2">
@@ -1550,7 +1569,7 @@ export default function InteractionClient() {
15501569
<div className="text-xs md:text-sm">
15511570
<div className="flex justify-between">
15521571
<span className="text-neutral-600 dark:text-neutral-400">Current price:</span>
1553-
<span className="font-medium text-right">{calculations.bullPrice.toFixed(4)} WETH</span>
1572+
<span className="font-medium text-right">{calculations.bullPrice.toFixed(4)} {poolData.base_symbol}</span>
15541573
</div>
15551574
<div className="flex justify-between">
15561575
<span className="text-neutral-600 dark:text-neutral-400">Underlying asset:</span>
@@ -1565,7 +1584,7 @@ export default function InteractionClient() {
15651584
<div className="text-xs md:text-sm">
15661585
<div className="flex justify-between">
15671586
<span className="text-neutral-600 dark:text-neutral-400">Current price:</span>
1568-
<span className="font-medium text-right">{calculations.bearPrice.toFixed(4)} WETH</span>
1587+
<span className="font-medium text-right">{calculations.bearPrice.toFixed(4)} {poolData.base_symbol}</span>
15691588
</div>
15701589
<div className="flex justify-between">
15711590
<span className="text-neutral-600 dark:text-neutral-400">Underlying asset:</span>
@@ -1673,6 +1692,8 @@ export default function InteractionClient() {
16731692
reserve={calculations.bearReserveNum}
16741693
supply={calculations.bearSupplyNum}
16751694
tokenAddress={poolData.bear_token.id}
1695+
baseDecimals={poolData.base_decimals}
1696+
baseSymbol={poolData.base_symbol}
16761697
/>
16771698
</div>
16781699

src/components/Forms/CreateFatePool.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,31 @@ export default function CreateFatePool() {
270270
const baseTokenAddress = formData.baseTokenAddress.trim();
271271
let oracleAddress: `0x${string}`;
272272

273+
// Pre-check base token decimals so creators get a clear message instead of an
274+
// opaque on-chain revert. Coin.sol rejects tokens without decimals()
275+
// (MissingDecimals) and tokens with more than 18 decimals (UnsupportedDecimals).
276+
if (!publicClient) {
277+
throw new Error("Public client not available");
278+
}
279+
let baseDecimals: number;
280+
try {
281+
baseDecimals = await publicClient.readContract({
282+
address: baseTokenAddress as `0x${string}`,
283+
abi: ERC20_ABI,
284+
functionName: "decimals",
285+
}) as number;
286+
} catch (decimalsError) {
287+
// A failure here is usually a token without decimals() (Coin.sol reverts MissingDecimals),
288+
// but it can also be a transient RPC/network error. Log the cause and keep the message honest.
289+
logger.error("Failed to read base token decimals()", decimalsError instanceof Error ? decimalsError : undefined);
290+
throw new Error(
291+
"Could not read decimals() on the base token. If it does not implement decimals(), pool creation will revert (MissingDecimals); otherwise this may be a temporary network/RPC issue, please retry."
292+
);
293+
}
294+
if (baseDecimals > 18) {
295+
throw new Error(`Base token uses ${baseDecimals} decimals. Only tokens with 18 or fewer decimals are supported (UnsupportedDecimals).`);
296+
}
297+
273298
// Handle oracle creation based on type
274299
if (formData.oracleType === "chainlink") {
275300
// Get the Chainlink adapter factory address
@@ -433,23 +458,16 @@ export default function CreateFatePool() {
433458

434459
if (initialDepositValue > 0) {
435460
try {
436-
// Get token decimals
437-
const decimals = await publicClient!.readContract({
438-
address: baseTokenAddress as `0x${string}`,
439-
abi: ERC20_ABI,
440-
functionName: "decimals",
441-
}) as number;
442-
443-
// Pre-check decimal places to avoid parseUnits error
461+
// Pre-check decimal places to avoid parseUnits error (decimals already read above)
444462
const decimalParts = formData.initialDeposit.split('.');
445463
const fractionalDigits = decimalParts.length > 1 ? decimalParts[1].length : 0;
446-
447-
if (fractionalDigits > decimals) {
448-
throw new Error(`Amount has too many decimal places. Maximum allowed: ${decimals} decimal places, but got ${fractionalDigits}.`);
464+
465+
if (fractionalDigits > baseDecimals) {
466+
throw new Error(`Amount has too many decimal places. Maximum allowed: ${baseDecimals} decimal places, but got ${fractionalDigits}.`);
449467
}
450468

451469
// Convert initial deposit to token units
452-
initialDepositAmount = parseUnits(formData.initialDeposit, decimals);
470+
initialDepositAmount = parseUnits(formData.initialDeposit, baseDecimals);
453471

454472
logger.debug("Initial deposit:", { initialDeposit: formData.initialDeposit, tokens: initialDepositAmount.toString(), units: "units" });
455473

src/lib/vaultUtils.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,16 @@ export async function getPoolStats(
202202
const poolContract = new ethers.Contract(vaultId, PredictionPoolABI, provider);
203203

204204
const stats = await poolContract.getPoolStats();
205-
205+
206+
// Reserves are base-token balances (base decimals); prices are oracle WAD (18).
207+
const baseTokenAddress: string = await poolContract.baseToken();
208+
const baseToken = new ethers.Contract(baseTokenAddress, ['function decimals() view returns (uint8)'], provider);
209+
const baseDecimals = Number(await baseToken.decimals());
210+
206211
return {
207-
bullReserves: Number(ethers.formatUnits(stats[0], 18)),
208-
bearReserves: Number(ethers.formatUnits(stats[1], 18)),
209-
totalReserves: Number(ethers.formatUnits(stats[2], 18)),
212+
bullReserves: Number(ethers.formatUnits(stats[0], baseDecimals)),
213+
bearReserves: Number(ethers.formatUnits(stats[1], baseDecimals)),
214+
totalReserves: Number(ethers.formatUnits(stats[2], baseDecimals)),
210215
currentPrice: Number(ethers.formatUnits(stats[3], 18)),
211216
lastPrice: Number(ethers.formatUnits(stats[4], 18)),
212217
};

0 commit comments

Comments
 (0)