For the complete documentation index, see llms.txt. This page is also available as Markdown.

Asset classification

How a registered asset is classified on two orthogonal axes — what its price tracks, and how its value accrues.

Every registered asset is classified on two orthogonal axes, kept separate because no single category answers both questions:

  • PegKind — what the price tracks. Pegged or Floating.

  • AccrualKind — how value accrues. None, Rebasing, or Appreciating.

Source: AssetKindRegistry.sol · AssetKind.sol


The two axes

PegKind means pegged to the quote asset — USD for the default quoteAsset == address(0) read — not "pegged to something in general". So USDC, USDT, DAI and GHO are Pegged, while wstETH is Floating even though it is rigidly related to stETH: it does not track USD.

AccrualKind

What grows

Examples

None

Neither balance nor rate

WETH, USDC, AAVE, WBTC

Rebasing

The balance grows; unit price stays put

stETH, eETH, aTokens

Appreciating

The rate grows; balance stays static

wstETH, sDAI, sGHO, sUSDe, ERC-4626 shares

Across the seven configured chains, 93 assets carry an accrual classification: 63 None, 27 Appreciating, 3 Rebasing.

Why two axes rather than one category

A single flat enum forces a wrong answer on assets already registered here, because the two properties genuinely cross:

None

Rebasing

Appreciating

Pegged

USDC

(an aToken — none registered; see below)

(cannot be asserted — see below)

Floating

WETH

stETH

wstETH, sDAI, sGHO, sUSDe, sUSDS

stETH and wstETH differ on the accrual axis only — both are Floating. Collapse the axes and that distinction is unexpressible.

sDAI is Floating, not Pegged — a yield-bearing wrapper has no peg. Its USD price is not $1 and rises with accrued yield, for the same reason wstETH is Floating. Appreciating is in fact the mechanism that makes an asset unpeggable: a growing redemption rate on a static balance moves the price off $1.


The peg axis is derived, not stored

PegKind is not stored anywhere. It is computed on read from the NAV's per-asset pegToleranceBps — the switch that enables the $1 peg guard:

There is exactly one authority for whether an asset is pegged, so there is no second copy to fall out of step and no cross-check needed to police one. Of the 93 classified assets, 25 carry a non-zero peg tolerance and therefore read as Pegged.

Because the value is read live rather than snapshotted at classification time, retuning a tolerance is immediately reflected.

The two empty cells are empty for different reasons

  • Pegged + Rebasing is real and reachable. An aToken for USDC has a growing balance at a $1 price. No registered asset occupies the cell, which makes it a forward-compatibility slot rather than a contradiction.

  • Pegged + Appreciating cannot be asserted. Nothing writes a peg kind, so it cannot be declared for an asset. It remains reachable as a read: set a non-zero peg tolerance on an asset stored as Appreciating and the pair reads back Pegged + Appreciating, because the peg is computed from that tolerance. That is a true report of the configuration, not an inconsistency to be caught.

Do not use PegKind as a staleness or safety oracle. It reports what an asset's price is supposed to track. It says nothing about whether the current price is fresh, or whether the asset is holding its peg right now — those are stalePriceAssets and the irregular signal.


Reading an asset's description

One call on the NAV Calculator returns everything public about an asset — both classification axes and its display labels. A consumer needs only the NAV address it already holds, and never has to learn that a registry exists:

It returns the enums rather than raw uint8s, so callers get a typed ABI rather than two integers to interpret.

getAssetInfo is declared on NAVCalculator itself, not on INAVCalculator. Bindings generated from the interface alone will not carry it. Generate against the concrete contract, or add the one signature by hand.

The narrower reads, the batch form and the probe live on the registry, which the NAV names via assetKindRegistry():

What a caller actually receives

The two halves of getAssetInfo fail differently, and the difference is deliberate:

  • The classification half fails closed. An unclassified asset reverts AssetNotClassified rather than reporting a default. The zero value of AccrualKind is None — a real classification held by 63 of the 93 configured assets — so a defaulting reply would be indistinguishable from a configured one. Probe with isClassified when an unclassified asset is an expected state.

  • The labels half never fails. getAssetLabels does not revert; an unlabelled asset returns an empty array, which is a real answer rather than a gap.

That asymmetry is the whole rule: there is no defensible default classification, but "no labels" is the honest answer.


Display labels

Alongside the two axes, an asset carries an ordered list of display labels — free text for presentation, which nothing on-chain parses.

Order is significant, and the list is not a set. Index 0 is the protocol or issuer; later indices narrow to the product or kind:

That broad-to-narrow order is the same one protocolName() and positionLabels() compose in — but the content differs, and the difference matters when you are writing the array. Asset labels carry the protocol at index 0. Position labels never do: there, protocolName() supplies the protocol separately and positionLabels() starts at the product (["Market", "wstETH/USDC"], ["Withdrawal Queue", "stETH"]).

So the two are alike in ordering and opposite in what index 0 holds. Reading either array as an unordered set discards the only structure it has.

At most 4 labels, each at most 32 bytes, none empty — TooManyAssetLabels, AssetLabelTooLong and EmptyAssetLabel reject the rest at write time. Zero labels is legitimate, not a misconfiguration.

A wrong label can be corrected in place, without touching the asset's registration or its price feeds:

It replaces the whole array rather than editing an element, and it can revert six ways. An integrator decoding errors needs to handle all six — the role gate, two of its own, and three content bounds it shares with registerAsset:

revert
when

NotAuthorized

the caller does not hold the NAV's MANAGER role — checked first, so the likeliest one in practice

AssetNotRegisteredForLabels

the asset is not registered on the NAV

EmptyAssetLabelSet

the array is empty

TooManyAssetLabels

more than 4 labels

AssetLabelTooLong

an element exceeds 32 bytes

EmptyAssetLabel

an element is the empty string

setAssetLabels replaces the whole list; it cannot empty one. It rejects an empty array, so through this call the transitions are none → some and some → other only. Labels do still go from some → noneunregisterAsset clears them — but that ends the registration, which is not a label operation.

The per-element bounds are identical on both write paths — at most 4 labels, 32 bytes each, no empty element — so a label list that could not be registered cannot be set either. They differ in two places: the empty array, which registerAsset accepts (meaning "leave labels alone") and setAssetLabels rejects; and their preconditions, which are opposites — registerAsset requires the asset to be absent from the NAV, setAssetLabels requires it to be present. They are not interchangeable.

An asset can also be unlabelled because it never went through registerAsset: the chain's native asset is registered during initialization rather than by that call, so in practice setAssetLabels is how it gains labels.

The facade's unregisterAsset drops an asset's labels; clearAssetKind keeps them. The two look interchangeable and are not:

  • clearAssetKind ends the classification while the asset stays registered on the NAV — its labels still describe a live asset, so they are kept.

  • AssetKindRegistry.unregisterAsset ends the registration, so it deletes the labels outright — after this call, and only this one, a later re-registration starts from none.


Governance

Registering an asset, classifying it and labelling it are one operation:

AssetKindRegistry.registerAsset stores the accrual kind and the labels, then forwards to the NAV's own registerAsset in the same transaction, so if either half reverts the whole transaction reverts and neither takes effect. An asset therefore cannot become registered-but-unclassified through a partly-applied change. Unregistration works the same way, and additionally drops the labels.

Mutations — registerAsset, unregisterAsset, setAssetKind, setAssetKinds, clearAssetKind, setAssetLabels — are gated on the NAV's MANAGER role:

The registry holds no roles of its own and has no separate admin. Authority is exactly the NAV's MANAGER set — one set of humans for the whole system, rather than a second contract with its own governance to keep in step. The NAV address is set in the registry's constructor and immutable.

The pointer in the other direction is not immutable: a MANAGER aims the NAV at a registry with setAssetKindRegistry, which rejects address(0). It is re-settable on purpose — the registry is a plain contract rather than a proxy, so replacing it means deploying a new one and re-pointing the NAV.

The stored axis is written from per-asset configuration, where the accepted values are None, Rebasing and Appreciating, case-exact. A pre-deploy check rejects an absent, empty, mis-cased or unrecognised value — but it validates the value, not the field set: a misspelled key (accrualkind, accrual_kind) is silently ignored and reaches the deploy as if the field were absent.

Last updated