Skip to main content

Yield factories

Three owner-gated factories deploy and track the yield stack, one instance per wrapper: PPYieldTokenFactory for the PPYieldToken wrappers, PPYieldTokenZapFactory for their zaps, and PPRouterFactory for their routers. They are kept separate so that none embeds another's creation bytecode (each stays clear of the contract-size limit) and each has one job; the deploy script orchestrates the three in order.

All three are Ownable and all three override renounceOwnership to revert, so their only owner-gated function can never be permanently bricked. Ownership is transferable but never droppable. Every instance they create is immutable (no proxy): "upgrading" a wrapper, zap, or router means deploying a new one through the factory and re-pointing relayer and SDK configuration, never changing code in place.

Genuineness

Each factory is bound to one trusted Aave v3 Pool and only creates instances for that pool's aTokens. The check is IAToken(aToken).POOL() == POOL, and the underlying is read from IAToken(aToken).UNDERLYING_ASSET_ADDRESS() rather than taken from the caller. A wrapper that lied about its asset() would at worst produce a useless zap or router (owner self-harm), never a working instance over a spoofed token, and the wrapper seed below goes through the real POOL.supply, which only a genuine aToken satisfies.

PPYieldTokenFactory

createWrapper(address _aToken, uint256 _feeBps, address _admin, string _name, string _symbol, uint256 _seedAssets) → wrapper Owner only. Requires the aToken belongs to POOL, no wrapper exists for it yet, and _seedAssets != 0. Deploys the wrapper with the given fee, admin, name, and symbol, then seeds it: the factory supplies _seedAssets of the underlying to Aave, deposits the resulting aToken into the new wrapper, and sends the minted shares to 0xdEaD, where they are locked forever.
wrapperFor(address _aToken) → address The wrapper deployed for an aToken, or zero.
POOL() → address The trusted Aave pool.

The dead-share seed is the belt to the decimals-offset braces: a wrapper is never empty, so the ERC-4626 first-depositor inflation attack is neutralised regardless of the asset's decimals. A wrapper deployed directly, bypassing the factory, loses the seed and keeps only the offset.

PPYieldTokenZapFactory

createZap(address _vault) → zap Owner only. Reads the wrapper's aToken, requires it belongs to POOL and that no zap exists for the wrapper, derives the underlying from the aToken, and deploys a zap over (vault, POOL, underlying).
zapFor(address _vault) → address The zap for a wrapper, or zero.
zapsCount() → uint256 / allZaps(uint256 i) → address Enumeration in deployment order.

Zaps hold no funds, so this factory grants no power over user assets.

PPRouterFactory

createRouter(address _vault, address _zap) → router Owner only. Deploys an ERC-20-mode router for the wrapper: the audited USDC-style flow.
createRouterNative(address _vault, address _zap) → router Owner only. Deploys a native-mode router for a wrapper over a wrapped-native aToken (aWETH, giving ppETH), so users deposit and receive the native coin. See PPRouter.
routerFor(address _vault) → address The router for a wrapper, or zero. One router per wrapper regardless of mode.
routersCount() → uint256 / allRouters(uint256 i) → address Enumeration in deployment order.
POOL(), ENTRYPOINT(), POOL_VAULT(), DUST_RECIPIENT() The Aave pool every router is checked against, and the entrypoint, vault, and dust recipient every router is given.

Both create functions share one internal path: they require the wrapper's aToken belongs to POOL, that no router exists for the wrapper, derive the underlying, and construct the router. The router's own constructor then verifies the supplied zap wraps exactly that (vault, underlying) pair, and a native-mode router additionally probes the underlying with a zero-value IWETH9.deposit, so a mismatched or non-WETH configuration fails at deploy time rather than on a user's first transaction.

Errors you'll see

PP*Factory_NotAavePoolAToken() The aToken's POOL() is not this factory's pool.
PP*Factory_AlreadyDeployed() An instance already exists for that aToken or wrapper.
PPYieldTokenFactory_ZeroSeed() createWrapper was called with _seedAssets == 0.
PP*Factory_RenounceDisabled() Someone called renounceOwnership.

Source: v2-monorepo/packages/contracts/src/contracts/PPYieldTokenFactory.sol, PPYieldTokenZapFactory.sol, PPRouterFactory.sol