Skip to content

Build1 publisher3 min readPublished

A Token-2022 mint can shrink every holder's displayed balance without a single transaction

Interest-bearing and scaled-UI-amount mints tell a Solana RPC node to compute the display amount from the mint config and its own clock, while the archive behind long-tail history stores the un-scaled number.

The Engineer · Build desk

What happened

  • An interest-bearing Token-2022 mint stores a rate in basis points per year, and the displayed balance is the fixed raw amount compounded continuously since initialisation, with nothing accruing on chain.
  • A scaled-UI-amount mint stores a multiplier, optionally with a replacement multiplier and the timestamp it takes effect, and the displayed balance is the raw amount times whichever multiplier is in force.
  • An RPC serving a parsed token account for these mints evaluates the extension config against the node's current unix timestamp, so the same account queried a minute apart returns a different display amount.
  • The parsed balance leaves the nullable uiAmount field empty for both extensions, so amount and uiAmountString are the only lossless fields a client can read.
  • The archive behind long-tail transaction history regenerates the decimal string from amount and decimals on read, and stores a null uiAmount as 0.0.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • contradiction A live node and the archive answer the same historical balance question with different numbers, so which backend a tool queries decides what a user is shown.
  • exposure A holder of a negative-rate mint watches the displayed balance fall while the transaction history stays complete and untouched, and there is no on-chain event to reconcile the decline against.
  • constraint Cost-basis and accounting exports built on archived history cannot be repaired downstream, because the correct figure depends on a clock reading the archive never kept.
  • decision Quote engines and portfolio views now have to choose between refetching the mint config on every valuation and pricing a quantity they cannot vouch for.

Client code that wants a human-readable balance usually takes `amount`, divides by ten to the power of `decimals`, and renders the string. For an interest-bearing or scaled mint that produces the plain, un-scaled value, which the write-up on dev.to says is simply wrong [8]. Arithmetic on `uiAmount` fails differently: it hits a null at exactly the mints where the display and the raw integer diverge [8]. The null is deliberate on the node's side [7].

The rate on an interest-bearing mint is a signed 16-bit integer in basis points per year [9]. That puts the representable range at -32,768 to 32,767 basis points, or about -327.68% to +327.67% a year [16]. Signed, in a field a product team would probably have specified as unsigned. Negative rates work as designed, so a mint can be configured so that every holder's displayed balance declines over time with no transfer, burn or event in anyone's history [9]. The post calls that a legitimate instrument for some products and an unlabelled drain in others [22].

Because the display amount is evaluated against the node's own bank timestamp, two nodes whose clocks differ by one slot can answer the same query with two different amounts at the same moment [6]. Mainnet slot time is 300 ms today and 250 ms from epoch 1037 [6]. That narrows the one-slot disagreement window by 50 ms, roughly 17% [17].

Anything that replays a wallet out of the archive reads un-scaled amounts for these mints, and zeros where the live node declined to return a number [11]. Cost-basis tools, portfolio trackers and explorers that lean on archived history will present a balance that never existed, according to the post [11]. The live node is correct because it evaluates the extension against the clock; the archive replays a stored integer [12].

Whether this reaches your product turns on one condition: whether the mints you index actually carry either extension with a non-zero rate or a multiplier other than one. The post does not say how many mainnet mints use either extension [20]. If you index a fixed list of major tokens and none of them is a Token-2022 rebasing wrapper, none of this touches you. If a user can hand your indexer an arbitrary mint, your archive path is already emitting un-scaled numbers.

The fix is storage. Keep `amount`, and keep the mint's extension config and its timestamps for every date you report on; the post's guidance for tax and accounting work is that the raw amount and the extension config at each date are the only honest inputs [13]. Read the config by fetching the mint account with parsed encoding, where the interest-bearing extension exposes the current rate, the pre-update average rate, and the initialisation and last-update timestamps [15]. For pricing, use `amount` with the current multiplier or rate, since a quote built on a stale display is a quote for the wrong quantity [14]. A scheduled multiplier is readable before it takes effect, which the post calls the closest thing Solana has to a pre-announced rebase [18].

What to watch

  • Any RPC or indexer changelog that starts persisting the mint extension config alongside the raw amount instead of a regenerated decimal string.
  • A wallet or explorer history view that labels a display revaluation differently from a transfer.
  • Published sourcing from a tax or cost-basis vendor on where it reads balances for Token-2022 mints.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories