fix(market): quote the royalty from basis points, not a float

ERC-2981 quotes an amount against a sale price rather than publishing a
rate, so the rate is read by asking for one whole token and dividing.
Doing that in floating point printed 250 basis points as
2.4999999999999996%. rateLabel scales by 10,000 in bigint first, so the
integer the contract stores comes back out intact.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-29 18:18:59 -07:00
co-authored by Hanzo Dev
parent f216bef77b
commit 87a4f1dff8
4 changed files with 15 additions and 9 deletions
+2 -3
View File
@@ -3,7 +3,6 @@
import { ExternalLink } from '@luxfi/ui/icons'
import { useSearchParams } from 'next/navigation'
import { Suspense, useEffect, useMemo } from 'react'
import { formatUnits } from 'viem'
import { useReadContracts } from 'wagmi'
import { ItemCard } from '@/components/ItemCard'
import { Page } from '@/components/Page'
@@ -12,7 +11,7 @@ import { Stat } from '@/components/Stat'
import { Card } from '@/components/ui/card'
import type { ChainState } from '@/hooks/chain'
import { useCollection, useCounters, useHolders, useTransfers, useTransferIds } from '@/hooks/queries'
import { ERC165_ABI, ERC2981_ABI, ERC721_ABI, INTERFACE, ONE } from '@/lib/contracts'
import { ERC165_ABI, ERC2981_ABI, ERC721_ABI, INTERFACE, ONE, rateLabel } from '@/lib/contracts'
import { idFor } from '@/lib/logs'
import { addressUrl, type Chain } from '@/lib/registry'
import { shortenAddress } from '@/lib/utils'
@@ -68,7 +67,7 @@ function Declares({ chain, address }: { chain: Chain; address: string }) {
</p>
{rate ? (
<p className="leading-relaxed text-muted-foreground">
royaltyInfo quotes {Number(formatUnits(rate[1], 18)) * 100}% of a sale to{' '}
royaltyInfo quotes {rateLabel(rate[1])} of a sale to{' '}
<code className="font-mono text-xs">{rate[0]}</code>. That is what the contract would
enforce on a marketplace that honours it; none is deployed on Lux, so nothing enforces it
today.
+3 -3
View File
@@ -15,6 +15,7 @@ import {
GENESIS_ABI,
GENESIS_TOKEN_ABI,
ONE,
rateLabel,
} from '@/lib/contracts'
import * as links from '@/lib/links'
import { addressUrl, type Chain } from '@/lib/registry'
@@ -72,7 +73,7 @@ function Collection({ chain, address }: { chain: Chain; address: `0x${string}` }
/>
<Stat
label="Royalty"
value={rate ? `${Number(formatUnits(rate[1], 18)) * 100}%` : null}
value={rate ? rateLabel(rate[1]) : null}
note="declared through ERC-2981"
/>
<Stat
@@ -90,8 +91,7 @@ function Collection({ chain, address }: { chain: Chain; address: `0x${string}` }
Mini, 1M for Nano, and each token below reports its own.
</p>
<p className="text-muted-foreground">
The collection declares a {rate ? `${Number(formatUnits(rate[1], 18)) * 100}%` : ''}{' '}
royalty through ERC-2981, which a marketplace has to read and honour. There is no
The collection declares a {rate ? rateLabel(rate[1]) : ''} royalty through ERC-2981, which a marketplace has to read and honour. There is no
marketplace contract on any Lux chain to read it, and this collection&rsquo;s own{' '}
<code className="font-mono text-xs">market</code> address is zero.
</p>
+2 -3
View File
@@ -5,7 +5,6 @@ import { useQuery } from '@tanstack/react-query'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { Suspense, useMemo } from 'react'
import { formatUnits } from 'viem'
import { useReadContracts } from 'wagmi'
import { GenesisMeta } from '@/components/Genesis'
import { Page } from '@/components/Page'
@@ -13,7 +12,7 @@ import { Source } from '@/components/Source'
import { Card } from '@/components/ui/card'
import type { ChainState } from '@/hooks/chain'
import { useCollection, useTransferIds, useTransfers } from '@/hooks/queries'
import { ERC165_ABI, ERC2981_ABI, ERC721_ABI, GENESIS, INTERFACE, ONE } from '@/lib/contracts'
import { ERC165_ABI, ERC2981_ABI, ERC721_ABI, GENESIS, INTERFACE, ONE, rateLabel } from '@/lib/contracts'
import * as links from '@/lib/links'
import { idFor } from '@/lib/logs'
import { addressUrl, txUrl, type Chain } from '@/lib/registry'
@@ -203,7 +202,7 @@ function Body({ state, slug, address, id }: { state: Ready; slug: string; addres
{rate ? (
<div>
<span className="text-muted-foreground">Royalty </span>
{Number(formatUnits(rate[1], 18)) * 100}% to{' '}
{rateLabel(rate[1])} to{' '}
<span className="font-mono text-xs">{rate[0]}</span>, declared through ERC-2981.
</div>
) : null}
+8
View File
@@ -74,6 +74,14 @@ export const INTERFACE = {
/** The unit royaltyInfo is quoted against, so the answer reads as a rate. */
export const ONE = 10n ** 18n
/**
* The quote for one whole token, as a percentage. Basis points are integers on
* chain, so this rounds off the float that dividing by 1e18 introduces —
* 250 bps came back as 2.4999999999999996 before.
*/
export const rateLabel = (quoted: bigint) =>
`${Number((quoted * 10_000n) / ONE) / 100}%`
/**
* GenesisNFTs.sol keeps a record per token — `mapping(uint256 => TokenMeta)
* public tokenMeta` — and its generated getter answers on chain today. It is