Yield shares
A yield note is an ordinary note whose tokenId is a share token rather than the asset a user thinks in. The user deposits USDC and holds a note denominated in ppUSDC shares; the share count never changes, and each share is redeemable for more USDC as Aave interest accrues. The pool learns nothing new to support this. It shields ppUSDC exactly as it shields any other ERC-20, and all of the yield machinery sits outside the pool as a wrapper, a zap, and a processor router.
Why shares, not a rebasing balance
If the pool shielded a token whose balance rebased, every note's value would drift with time, and the drift would be a clock. A note created when 1 aUSDC was worth 1.00 USDC and spent when it was worth 1.05 would reveal roughly how long it had been held, because the circuit enforces value conservation on the committed values and any change would have to be accounted for publicly.
A fixed share count avoids that entirely. The number committed in the note is the same at deposit and at spend, so conservation holds with no time-dependent term, and yield is realised only at the moment shares are unwrapped back to the underlying. Nothing inside the pool distinguishes a share note held for a day from one held for a year.
Properties
- Shares are always 18 decimals. PPYieldToken fixes the ERC-4626 decimals offset to
18 - assetDecimals, so ppUSDC over 6-decimal aUSDC carries a 10^12 virtual-share buffer against first-depositor inflation, on top of the dead shares the factory locks at creation. - The rate comes from live backing. A share's value is
totalAssets() / totalSupply(), andtotalAssets()is the wrapper's current aToken balance net of protocol fees. It is never stored, so it cannot go stale. - The protocol fee is on yield only. The wrapper charges
feeBps(at most 20%) of the balance increase since its last checkpoint. Principal is never charged, and a fee change is never retroactive. - Asset configuration is share-denominated. Because ppUSDC has 18 decimals, its
minAmountandmaxRelayFeeon the Entrypoint are 10^12 times the plain-USDC figures for the same dollar value, and a share-denominated floor drifts upward in dollar terms as the rate rises. - A yield note is still a pool note. It can be transferred privately, withdrawn as ppUSDC through the plain PrivacyPoolRelay, or ragequit. Only the unwrap back to USDC needs the router, and only the unwrap depends on Aave liquidity.
What the share count leaks, and what to do about it
The fixed share count protects the time dimension, but it moves the anonymity-set question to the amount dimension. Two users who each deposit 100 USDC at different exchange rates receive different share counts, and notes of different value are distinguishable. If deposits are sized in round USDC amounts, the share count records the rate at deposit time.
The mitigation is to size deposits in shares rather than in the underlying. PPRouter.depositExactShares exists for exactly this: the note commits to a fixed share value and the underlying actually spent floats up to a maxUnderlyingIn cap. An application that offers a small menu of standard share amounts keeps yield notes bucketed the way plain notes are; one that lets users type arbitrary USDC amounts does not. This is an application-layer obligation, not something the contracts can enforce.
Implications for SDK callers
- Size from shares. Use
sharesForUnderlyingwhen the input is a mint principal, orsizeSharesForBudgetwhen the input is what the wallet is willing to spend all-in; both live on the yield session returned bysession.yieldFor(token). See Yield deposit. - Read value through
previewRedeem. The UI-facing value of a yield note isPPYieldToken.previewRedeem(shares), which the SDK wraps on the yield session'stokeninteractor. Never show a user the raw share count as a balance. - A withdrawal to USDC is a normal transact whose
processoris the router, so it cannot be an item in a batch withdrawal: the batch relay is itself the processor. Withdrawing the shares as shares through the plain relay is the way to move many yield notes at once. - After a ragequit the owner holds loose ppUSDC in their wallet, not USDC. PPYieldTokenZap
.redeemToUnderlyingconverts it in one call, andPPYieldToken.redeemexits to the aToken without touching Aave at all.