Skip to main content

PPYieldTokenZap contract

PPYieldTokenZap is a stateless helper that converts between an underlying token and a PPYieldToken share in one call: it supplies the underlying to Aave, wraps the resulting aToken into shares, and does the reverse on the way out. It is the only place in the yield stack that calls Aave, which is what keeps PPRouter protocol-agnostic. One zap is deployed per wrapper by PPYieldTokenZapFactory.

What it does

The zap holds no funds between transactions and has no owner. Its four entry points are the two directions of the same conversion, plus an exact-share variant and a quote:

depositUnderlying(uint256 _underlyingAmount, address _receiver) → shares Pulls the underlying from the caller, supplies it to Aave with the zap as the onBehalfOf, wraps exactly the aToken that supply produced into the wrapper, and mints the shares to _receiver.
buyFixedYieldToken(uint256 _shares, address _receiver, uint256 _maxUnderlyingIn) → underlyingIn Mints an exact share count. Sizes the underlying as previewMint(_shares) + 2, reverts PPYieldTokenZap_SlippageExceeded if that exceeds _maxUnderlyingIn, supplies to Aave, requires the aToken received covers the mint (PPYieldTokenZap_InsufficientWrap), mints exactly _shares to _receiver, and forwards any residual aToken dust from this call to _receiver. This is the leg PPRouter.depositExactShares uses.
redeemToUnderlying(uint256 _shares, address _receiver) → underlyingOut Pulls the shares from the caller, redeems them at the wrapper for aToken, withdraws that aToken from Aave, and pays the underlying to _receiver. This is also the one-call exit for anyone holding loose shares, for example after a ragequit of a yield note.
quoteBuy(uint256 _shares) → underlyingNeeded previewMint(_shares) + 2, the exact figure buyFixedYieldToken will pull. PPRouter.quote is built on it.

The 2-unit supply buffer

A single Aave supply(x) credits either x or x - 1 units of aToken because of the scaled-balance round trip, so an exact mint that supplied precisely previewMint(shares) could come up one unit short and revert. The zap over-supplies by _SUPPLY_ROUNDING_BUFFER = 2 on every exact mint. That buffer is the only value a depositor forfeits per exact mint, and it is not kept: whatever aToken is left after the mint is forwarded to the receiver as dust (a router receiving it sweeps it later with skim).

Balance-delta accounting

Every amount the zap credits or withdraws is measured as the change in its own aToken balance around the Aave or wrapper call, never as the absolute balanceOf. depositUnderlying wraps only the aToken this supply produced; redeemToUnderlying withdraws only the aToken this redeem produced; buyFixedYieldToken checks the wrap and forwards dust against a pre-supply baseline.

This is what makes the zap safe to leave permissionless. If it read absolute balances, an attacker could donate aToken to the zap and have the next caller sweep the donation into their own shares or their own payout. With deltas, a pre-donated balance is simply left untouched, and the zap never accrues value beyond such donations.

Immutables

VAULT() → address The PPYieldToken wrapper this zap serves.
POOL() → address The Aave v3 Pool used for supply and withdraw.
UNDERLYING() → address The token supplied to Aave, e.g. USDC.
A_TOKEN() → address VAULT.asset(), the aToken the wrapper is backed by.

RelaySwaps reads VAULT, A_TOKEN, and UNDERLYING when it allowlists a zap, and PPRouter's constructor checks that its zap's VAULT and UNDERLYING match its own, so a misconfigured pairing fails at deploy time rather than on a user's withdrawal.

Errors you'll see

PPYieldTokenZap_ZeroAddress() Constructor received a zero vault, pool, or underlying.
PPYieldTokenZap_ZeroAmount() A zero underlying amount or share count was passed.
PPYieldTokenZap_SlippageExceeded(uint256 required, uint256 maxUnderlyingIn) The exact mint needs more underlying than the caller's cap. Re-quote and retry; the rate moved between quote and execution.
PPYieldTokenZap_InsufficientWrap(uint256 received, uint256 needed) Aave credited less aToken than the mint needs, even with the buffer. Not expected on a standard reserve; indicates unusual index rounding for that asset.

Events

ZappedIn(address sender, address receiver, uint256 underlyingIn, uint256 shares) Emitted by depositUnderlying.
ZappedInExact(address sender, address receiver, uint256 underlyingIn, uint256 shares) Emitted by buyFixedYieldToken.
ZappedOut(address sender, address receiver, uint256 shares, uint256 underlyingOut) Emitted by redeemToUnderlying.

Source: v2-monorepo/packages/contracts/src/contracts/PPYieldTokenZap.sol