Frontend: FeeTransparency hardcodes 18 decimals; silent drift on non-18-decimal acceptedAsset #110

Closed
opened 2026-05-02 01:25:05 +00:00 by Brouie · 5 comments
Brouie commented 2026-05-02 01:25:05 +00:00 (Migrated from gitlab.com)

P3-low silent display gap: frontend/src/components/FeeTransparency.tsx hardcodes 18 decimals when formatting both the per-tx amount and per-sink shares from indexer-recorded FeesDistributed events. The contract FeeRouter.distributeFees(IERC20 token, uint256 amount) accepts any ERC20, so this assumption silently breaks if a TimeCurve deployment uses a non-18-decimal accepted asset (e.g. USDC at 6).

Repro

FeeTransparency.tsx line 161:

row.token — amount
  {formatCompactFromRaw(rawToBigIntForFormat(row.amount), 18)}

Line 209 (in formatFeeSharesJsonForDisplay):

const amt = formatCompactFromRaw(rawToBigIntForFormat(String(s[i])), 18);

Comment immediately above the helper acknowledges the assumption:

/** `shares` from indexer are per-sink token amounts (wei); 18 dp assumed (see amount row). */

But the indexer row carries row.token (the actual token address that was distributed), and IERC20Metadata.decimals() is the source of truth.

Why this matters

Current CL8Y deployment uses an 18-decimal acceptedAsset, so the display is correct today. But:

  • FeeRouter.distributeFees is token-agnostic at the contract level — operators can route any ERC20 through it.
  • A future TimeCurve instance with USDC (6 dp), USDM (18 dp — also coincidentally fine), or any other asset would silently render 1.0 USDC as 0.000001 USDC in the FeeTransparency panel — 12 orders of magnitude off.
  • The acceptedAsset for the live YO deployment is configurable per-TimeCurve, so this is a multi-deployment concern.

No crash, no on-chain incorrectness — just silent display drift on a trust-sensitive panel.

Suggested fix

Replace the hardcoded 18 with a useReadContracts lookup keyed on the unique row.token addresses in the visible page:

const tokenAddrs = useMemo(
  () => Array.from(new Set(feesDistributed?.map(r => r.token) ?? [])),
  [feesDistributed],
);
const { data: decimalsResults } = useReadContracts({
  contracts: tokenAddrs.map(addr => ({
    address: addr,
    abi: erc20MetadataAbi,
    functionName: "decimals" as const,
  })),
  query: { enabled: tokenAddrs.length > 0 },
});
const decimalsByToken = useMemo(() => {
  const map = new Map<string, number>();
  decimalsResults?.forEach((r, i) => {
    if (r.status === "success") map.set(tokenAddrs[i], r.result as number);
  });
  return map;
}, [decimalsResults, tokenAddrs]);

Then in render: formatCompactFromRaw(rawToBigIntForFormat(row.amount), decimalsByToken.get(row.token) ?? 18).

18 stays as the fallback to preserve current behavior on cold-load before the decimals read resolves.

Same change applies to formatFeeSharesJsonForDisplay — needs the per-row token decimals threaded through.

Severity

P3-low. Not user-facing today (CL8Y is 18 dp). Future-fragile defensive gap on a trust-sensitive panel. Per 4/29 standing rule on aggressive filing.

Acceptance

  • FeeTransparency.tsx uses IERC20Metadata.decimals() from row.token rather than hardcoded 18 for both amount and shares formatting.
  • Fallback to 18 only when the decimals read has not resolved yet.
  • No regression on the current CL8Y display (18 dp) — values render identically.
  • Same decimals reads cached across the visible feed (deduped by token address).

cc @PlasticDigits

P3-low silent display gap: `frontend/src/components/FeeTransparency.tsx` hardcodes `18` decimals when formatting both the per-tx `amount` and per-sink `shares` from indexer-recorded `FeesDistributed` events. The contract `FeeRouter.distributeFees(IERC20 token, uint256 amount)` accepts any ERC20, so this assumption silently breaks if a TimeCurve deployment uses a non-18-decimal accepted asset (e.g. USDC at 6). ### Repro `FeeTransparency.tsx` line 161: ```tsx row.token — amount {formatCompactFromRaw(rawToBigIntForFormat(row.amount), 18)} ``` Line 209 (in `formatFeeSharesJsonForDisplay`): ```ts const amt = formatCompactFromRaw(rawToBigIntForFormat(String(s[i])), 18); ``` Comment immediately above the helper acknowledges the assumption: ```ts /** `shares` from indexer are per-sink token amounts (wei); 18 dp assumed (see amount row). */ ``` But the indexer row carries `row.token` (the actual token address that was distributed), and `IERC20Metadata.decimals()` is the source of truth. ### Why this matters Current CL8Y deployment uses an 18-decimal `acceptedAsset`, so the display is correct *today*. But: - `FeeRouter.distributeFees` is **token-agnostic** at the contract level — operators can route any ERC20 through it. - A future TimeCurve instance with USDC (6 dp), USDM (18 dp — also coincidentally fine), or any other asset would silently render `1.0 USDC` as `0.000001 USDC` in the FeeTransparency panel — 12 orders of magnitude off. - The `acceptedAsset` for the live YO deployment is configurable per-TimeCurve, so this is a multi-deployment concern. No crash, no on-chain incorrectness — just **silent display drift** on a trust-sensitive panel. ### Suggested fix Replace the hardcoded `18` with a `useReadContracts` lookup keyed on the unique `row.token` addresses in the visible page: ```ts const tokenAddrs = useMemo( () => Array.from(new Set(feesDistributed?.map(r => r.token) ?? [])), [feesDistributed], ); const { data: decimalsResults } = useReadContracts({ contracts: tokenAddrs.map(addr => ({ address: addr, abi: erc20MetadataAbi, functionName: "decimals" as const, })), query: { enabled: tokenAddrs.length > 0 }, }); const decimalsByToken = useMemo(() => { const map = new Map<string, number>(); decimalsResults?.forEach((r, i) => { if (r.status === "success") map.set(tokenAddrs[i], r.result as number); }); return map; }, [decimalsResults, tokenAddrs]); ``` Then in render: `formatCompactFromRaw(rawToBigIntForFormat(row.amount), decimalsByToken.get(row.token) ?? 18)`. 18 stays as the fallback to preserve current behavior on cold-load before the decimals read resolves. Same change applies to `formatFeeSharesJsonForDisplay` — needs the per-row token decimals threaded through. ### Severity P3-low. Not user-facing today (CL8Y is 18 dp). Future-fragile defensive gap on a trust-sensitive panel. Per 4/29 standing rule on aggressive filing. ### Acceptance - [ ] `FeeTransparency.tsx` uses `IERC20Metadata.decimals()` from `row.token` rather than hardcoded `18` for both amount and shares formatting. - [ ] Fallback to `18` only when the decimals read has not resolved yet. - [ ] No regression on the current CL8Y display (18 dp) — values render identically. - [ ] Same `decimals` reads cached across the visible feed (deduped by token address). cc @PlasticDigits
PlasticDigits commented 2026-05-02 04:43:34 +00:00 (Migrated from gitlab.com)

While this can be a serious security issue, in our case it is not a problem as the cl8y deployment is 18 decimals as is DOUB. HOwever, it does need to be fixed before conversion into full launchpad. Does not block current launch so marking low priortiy

While this can be a serious security issue, in our case it is not a problem as the cl8y deployment is 18 decimals as is DOUB. HOwever, it does need to be fixed before conversion into full launchpad. Does not block current launch so marking low priortiy
Brouie commented 2026-05-04 04:45:43 +00:00 (Migrated from gitlab.com)

mentioned in issue #126

mentioned in issue #126
Brouie commented 2026-05-05 23:51:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #164

mentioned in issue #164
Brouie commented 2026-05-07 05:07:46 +00:00 (Migrated from gitlab.com)

mentioned in merge request !379

mentioned in merge request !379
PlasticDigits commented 2026-05-07 12:00:07 +00:00 (Migrated from gitlab.com)

mentioned in commit efb6e0bce7

mentioned in commit efb6e0bce768d9482972681b935f6ec753941040
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-07 12:00:07 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#110
No description provided.