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.PeggedorFloating.AccrualKind— how value accrues.None,Rebasing, orAppreciating.
Whether a registry exists on a given chain is a deployment question, and this page is not its authority. Deployment addresses lists every live contract: if no AssetKindRegistry appears there for a chain, none is deployed on it and there is no address to call. What follows is what the contract implements wherever it is deployed.
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.
pegToleranceBps now does two jobs — read this before zeroing one. It is both a threshold ("flag drift beyond this") and a switch ("this asset should be worth $1"). Retuning a non-zero threshold — say 100 → 250 — leaves the classification untouched; it is crossing zero that flips it. The two jobs are independent everywhere except at that boundary.
The trap lives exactly there: zeroing the tolerance on a genuine stablecoin for an operational reason — a feed too noisy to guard, say — silently reclassifies that asset as Floating. Nothing reverts and nothing warns, and every off-chain consumer stops believing it is a stablecoin. Disabling the $1 guard on an asset that really is pegged needs its own field, not a zeroed tolerance.
The two empty cells are empty for different reasons
Pegged + Rebasingis real and reachable. An aToken for USDC has a growing balance at a$1price. No registered asset occupies the cell, which makes it a forward-compatibility slot rather than a contradiction.Pegged + Appreciatingcannot 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 asAppreciatingand the pair reads backPegged + 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
AssetNotClassifiedrather than reporting a default. The zero value ofAccrualKindisNone— a real classification held by 63 of the 93 configured assets — so a defaulting reply would be indistinguishable from a configured one. Probe withisClassifiedwhen an unclassified asset is an expected state.The labels half never fails.
getAssetLabelsdoes 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.
A NAV that is not wired to a registry reverts rather than answering. This is a different failure from an unclassified asset, and it looks nothing like an empty result: a consumer gets a revert, not a blank. Returning (Pegged, None) instead would be indistinguishable from a configured answer for a real stablecoin.
Read assetKindRegistry() to see which registry — if any — a given NAV points at. Wiring is a separate step from deploying either contract, so a NAV can be entirely correct while every classification read reverts.
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:
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 → none — unregisterAsset 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.
Never unregister an asset to fix its display strings. unregisterAsset is not a label operation. It tears down the asset's entire pricing configuration — every price feed, every monitor feed, both tolerances — and takes the asset out of the NAV until it is registered again, so every consumer's NAV loses its balance in the meantime.
Re-registering does not restore that configuration. It sets one primary feed; everything else is rebuilt by hand, in an order that matters, and pegToleranceBps returning to 0 silently flips a stablecoin's derived peg classification to Floating. Treat unregistration as removing the asset from the portfolio and re-onboarding it from scratch — see Price feeds for what that entails — not as an edit.
Correcting a wrong label requires none of it. That is what setAssetLabels is for.
This does not extend to adapter labels, and the asymmetry is deliberate. protocolName() and positionLabels(positionId) have no setter — changing either means redeploying that adapter.
The difference is where the label comes from. An asset label is stored data — one row, entered by hand, so a typo in it is a typo in that row and nothing else, and one write fixes it.
An adapter label is fixed at deployment, and applies to every position that adapter serves. Some adapters return a constant, some compute the label from the position, and some carry it as a constructor argument pinned at construction — but in all three cases it belongs to the deployed adapter, not to a row, so a wrong one is wrong for every position of that adapter at once.
A per-instance override table papers over the symptom one row at a time while leaving the cause in place. The objection is sharpest for the computed part of a label: many adapters derive it live from on-chain state, so pinning it per row would freeze exactly what makes it worth having — a label that follows the position rather than a stale record of it. The repair is to change whatever produces the label, the code or the constructor argument, and redeploy that adapter.
The facade's unregisterAsset drops an asset's labels; clearAssetKind keeps them. The two look interchangeable and are not:
clearAssetKindends the classification while the asset stays registered on the NAV — its labels still describe a live asset, so they are kept.AssetKindRegistry.unregisterAssetends the registration, so it deletes the labels outright — after this call, and only this one, a later re-registration starts from none.
That holds for the facade's unregisterAsset, not for the NAV's. A MANAGER calling NAVCalculator.unregisterAsset directly — permitted by design, the same two doors as registration — strands the labels: the registry still holds them, and the facade's unregisterAsset can no longer clear them because it reverts inside the NAV, which no longer has the asset.
The classification survives too, and this is the part that misleads: isClassified still returns true and getAssetInfo still answers rather than reverting, so a consumer sees a live, classified, plausibly-labelled asset that the NAV holds nothing for. Unregistration also cleared the asset's tolerances, so if it had a peg tolerance the derived peg now reads Floating — but most assets never had one, so an unchanged Floating is not evidence that nothing happened.
Getting out. Re-register through the facade with the correct labels: given a non-empty array, registerAsset overwrites whatever is there, deliberately, since registration is what defines an asset's labels. One transaction. Re-registering with an empty array does not clear the stale set — an empty list means "leave labels alone" — and if that has already happened, setAssetLabels replaces them.
The two calls have opposite preconditions, which is easy to get backwards: registerAsset requires the asset to be absent from the NAV and reverts InvalidArguments if it is already registered, while setAssetLabels requires it to be present and reverts AssetNotRegisteredForLabels if it is not.
Re-registration fixes the labels, not the pricing. It restores one primary feed and leaves the tolerances and every monitor feed cleared — treat that as the re-onboarding it is.
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.
That guarantee covers the facade, not the NAV. NAVCalculator.registerAsset is still callable directly by any MANAGER, and it writes neither classification nor labels — so a registered asset having a classification is a property of which door was used, not of the system. This is the case where getAssetInfo reverts AssetNotClassified for an asset that really is registered and really is being priced.
Both halves can now be repaired in place, in two transactions — setAssetLabels for the labels, setAssetKind for the classification. Neither requires unregistering, and neither touches the asset's price feeds.
The two do not validate alike, though. setAssetLabels checks that the asset is registered on the NAV and reverts AssetNotRegisteredForLabels if not — so a mistyped address is caught. setAssetKind has no such gate: it rejects only address(0), and will happily classify an address the NAV has never registered, emitting the event as though it were real. Nothing later reverts to reveal it.
What the facade still buys is atomicity, not exclusivity: registerAsset makes registration, classification and labels one transaction that cannot half-apply, whereas repairing after the fact leaves a window in which the asset is registered and priced but not yet classified — during which getAssetInfo reverts. Unregistering through the facade and re-registering remains the only way to end a label list, since setAssetLabels rejects an empty array — and it carries the cost described above.
Whether the two doors are narrowed to one is a deployment-time choice about who holds MANAGER, not something the contracts settle. Treat "registered" and "classified" as separate questions and probe with isClassified when the answer matters.
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.
"No roles of its own" is not "unprivileged" — both halves matter. Because registerAsset forwards to the NAV, and the NAV gates that call on its caller, the registry itself must hold MANAGER on the NAV. So the registry can register and unregister assets, and a fault in it reaches the NAV's asset set. What the design removes is a second role surface to keep in sync; it does not remove privilege from the system.
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