In Aave V3.7.0, scaledBalanceOf returns stored scaled aToken units before the supply-income index is applied. balanceOf returns those units multiplied by the reserve's current normalized income, rounded down. The index uses 27-decimal ray precision; token decimals are a separate display scale. Transfers move scaled units, so the requested amount and actual balance changes can differ through integer rounding.
Give each number its own unit
An aToken's wallet balance includes supplied principal and accrued supply income, as described in Aave's V3 tokenization documentation. The stored scaled balance is an input to that balance calculation. Treating both as ordinary token balances creates a reconciliation error before any transaction is considered.
This article follows the EVM reference implementation in Aave V3.7.0, pinned to its release commit cff15de6d127. It does not establish which release a particular deployed pool uses. V4 is outside this example. Older V3 tutorials can also use different rounding: Aave's V3.5 change notes document the move from earlier half-up operations to explicit rounding directions.
| Read or value | What it measures | How to use it |
|---|---|---|
| scaledBalanceOf(user) | Stored scaled aToken integer | Apply the matching income index once |
| balanceOf(user) | Current aToken balance in token base units | Apply token decimals for display |
| Normalized income | Reserve conversion index in rays | Divide its integer by 10^27 to interpret the multiplier |
| Token decimals | Places used to display a token amount | Keep separate from index precision |
The scaled-balance base contract exposes the stored integer. The pinned TokenMath library converts it to an aToken balance with downward rounding.
For valid inputs, the relationship is visibleRaw = floor(scaledRaw × index / RAY). Here, RAY equals 10^27, as defined in WadRayMath. Only after this conversion do you divide visibleRaw by the token's decimal scale.
The ERC-20 decimals guide covers that final display step. A six-decimal token does not make its reserve index six-decimal. Conversely, dividing a raw scaled balance by 10^27 would confuse the amount's unit with the index's precision.
Follow one supply through accrual and a transfer
The following is a constructed illustrative ledger. U is a fictional underlying token with six decimals; its aToken also uses six decimals. The indexes are stipulated observations, with no claimed time interval, rate or forecast. Wallet A supplies 1,000 U, wallet B starts empty, and A later makes an ordinary direct aToken transfer to B. Assume both operations succeed, A has no debt, and these wallets have no other position changes.
At supply, normalized income is 1.10 × RAY. The underlying contribution is 1,000,000,000 raw units. V3.7.0 calculates supplied scaled units by dividing by the index and rounding down. Thus floor(1,000,000,000 / 1.10) creates 909,090,909 scaled units. The integer calculation multiplies by RAY before dividing by the raw index; the decimal expression simply makes it readable.
Multiplying back and flooring gives 999,999,999 raw aToken units, displayed as 999.999999. The one-base-unit gap from the contribution comes from the two integer conversions. Preserve the actual 1,000 U cash flow instead of rewriting it to match the visible balance. The pinned supply logic separately records the supplied amount and passes calculated scaled units to the aToken mint.
Later, normalized income reaches 1.20 × RAY. A still holds 909,090,909 scaled units, now representing 1,090,909,090 raw balance units, or 1,090.909090 U. Accrual since the first observation is 90.909091 U. The absence of a change in scaled units does not mean income stopped.
| Observation | Index divided by RAY | A: scaled integer | A: visible balance | B: visible balance |
|---|---|---|---|---|
| Supply completed | 1.10 | 909,090,909 | 999.999999 | 0 |
| Before transfer | 1.20 | 909,090,909 | 1,090.909090 | 0 |
| Transfer completed | 1.20 | 700,757,574 | 840.909088 | 250.000002 |
| Later observation | 1.25 | 700,757,574 | 875.946967 | 260.416668 |
Visible balances in this table are in the fictional token's underlying-equivalent units. They are accounting amounts, with no assumed dollar price or claim about immediate withdrawal availability. The broader lending receipt-token guide separates that accounting claim from cash available for redemption.
Why the transfer changes slightly more than requested
A requests 250.000001 aTokens, or 250,000,001 raw units. At the 1.20 index, the transferred scaled quantity is rounded up: ceil(250,000,001 / 1.20) equals 208,333,335. This follows TokenMath's aToken transfer rule.
A's remaining scaled balance is 909,090,909 minus 208,333,335, giving 700,757,574. Converting that remainder produces 840,909,088 raw balance units. A's actual debit is therefore 1,090,909,090 minus 840,909,088, or 250,000,002 raw units. B's newly received scaled units also convert to 250,000,002 raw units.
Both changes exceed the requested amount by one token base unit, or 0.000001 U. This is the result of the selected integer inputs and rounding rules. It is not a transfer fee or a universal one-unit error bound.
The transfer conserves scaled units: A's 700,757,574 plus B's 208,333,335 equals the original 909,090,909. At the later 1.25 index, each wallet's balance is calculated independently. Their displayed amounts sum to 1,136.363635 U. Converting their combined scaled units once gives 1,136.363636 U. Flooring individual values and then adding can differ from flooring their sum; this difference does not imply that one wallet lost scaled units.
Read the transfer's two records separately
The ordinary transfer path in the pinned AToken source emits a Transfer event containing the requested unscaled amount. It also emits BalanceTransfer containing the actual scaled quantity and index. The IAToken interface explicitly identifies BalanceTransfer's value as scaled.
For this illustrative transfer, keep three observations distinct:
- Requested amount and ordinary Transfer value: 250,000,001 raw token units.
- BalanceTransfer value: 208,333,335 scaled units, with an index of 1.20 × RAY.
- Actual same-index balance debit and credit: 250,000,002 raw token units each.
That AToken path also emits events recognizing accrued income since a holder's previously recorded index. In the example, A's recognized accrual is 90,909,091 raw units. Those events describe income already included in the pre-transfer balance; adding the income again to that balance would count it twice.
A replay using only ordinary Transfer values would start at 999,999,999, add 90,909,091, and subtract 250,000,001. It would produce 840,909,089. The actual balance is 840,909,088. Even this one-transfer fixture leaves a one-unit mismatch. Use scaled-unit records and the matching conversion to explain it, rather than forcing the wallet balance to match a generic event ledger.
Use normalized income at the observation block
The conversion needs the index used for the current read. Read the pool's getReserveNormalizedIncome for the underlying asset at the chosen block; this is the input used by the pinned aToken's balanceOf. In the pinned reserve logic, normalized income returns the stored liquidity index when its update timestamp matches the block timestamp. Otherwise, it calculates accrued linear interest from the stored rate and timestamp and applies that factor to the stored index.
Consequently, a view read can produce a growing balance without writing a new balance to each holder's storage. A reserve's last stored checkpoint can also differ from its normalized income at a later block. Reading only that checkpoint can understate the amount returned by balanceOf.
The holder's getPreviousIndex value is another checkpoint: it records the index associated with that holder's previous accounting update. It helps explain recognized accrual events. It is not a substitute for the reserve's current normalized income. In the illustrative ledger, applying A's old 1.10 index immediately before the transfer would reproduce the earlier balance and miss the intervening accrual.
Keep supply and debt indexes distinct as well. The liquidity-index versus variable-borrow-index guide explains why an aToken supply claim and a variable-debt balance require different conversion inputs. A field named only “index” is too ambiguous for a record you intend to reproduce later.
Reconcile a real observation without guessing
Build a snapshot with these fields before interpreting a discrepancy. Read all state-dependent values at one explicit block reference, rather than issuing unrelated latest-state calls and assuming they match.
| Record | Check to perform |
|---|---|
| Chain, pool and aToken addresses | Identify the exact reserve and externally queried token |
| Underlying and implementation | Verify token identity and the implementation active at that block |
| Block reference and token decimals | Bind every integer and display scale to the same observation |
| Scaled balance and normalized income | Recompute the visible raw balance with the pinned rounding rule |
| Contract balanceOf result | Compare exact integers before display rounding |
| Transactions between snapshots | Separate supply, withdrawal, transfers and liquidation from accrual |
First compare calculated visibleRaw with the contract result. An exact match validates that conversion at the selected state; it does not verify every historical cash flow. If the result differs, check the block reference, reserve identity and implementation before introducing a rounding tolerance. A blanket tolerance can hide an entirely wrong index.
Then explain changes in scaled units from completed operations. Keep underlying receipts separate from aToken transfers: moving aTokens between wallets does not itself redeem the underlying. If liquidation moved collateral, identify that action explicitly; the close-factor versus liquidation-bonus guide provides the separate debt-and-collateral ledger. A falling aToken holding after seizure is not negative supply income.
Finally, retain exact integers even if the interface rounds them for readability. Check fixtures with a nonintegral division, an empty recipient and a later index observation, as this example does. Test any static wrapper separately because its share conversion introduces another accounting relationship.
For each observation, save the scaled integer, normalized-income integer, token decimals and exact balanceOf result together. For each movement, add the requested amount and actual scaled change. Those records let another reader reproduce both the visible balance and the small differences that a rounded screen or ordinary Transfer log can conceal.
