UltraYield V2 is delivered as an upgrade of the existing V1 proxies, not only as a fresh deployment. The proxy addresses, share token, and user balances are preserved; only the implementation behind each proxy changes. A one-time migration hook, initializeV2, moves live state into the V2 shape after the implementation is swapped.
This document is for integrators upgrading their tooling and for auditors reviewing the migration path. For the rebuilt pricing stack see Oracles; for the fee model see Economics; for the redeem lifecycle see Flows.
flowchart LR
V1[Live V1 proxy<br/>holds user funds] -->|swap implementation| V2impl[V2 implementation<br/>concrete variant]
V2impl -->|initializeV2 — reinitializer 2| Migrated[Migrated V2 vault<br/>left paused]
Migrated -->|verify, then unpause| Live[Open for deposits/redeems]
UltraVault is now an abstract base, so the implementation behind each proxy is a concrete variant — CompliantUltraVault or AllowlistUltraVault.
initializeV2(address instantRedeemExitpoint, address upgradeModule, address freezeRegistry) is a reinitializer(2) hook run once on a live proxy that already holds user funds:
- Migratability guard. Reverts
NotMigratable()unless the deprecated V1_fundsHolderslot is non-zero. A populated slot is the proof that the proxy carries genuine V1 state; an empty slot means there is nothing to migrate. This is the only guard oninitializeV2— there is no caller-role gating — so the call must run atomically with the implementation swap (see Step-by-step upgrade) to prevent a front-run that capturesupgradeModule(). - Address registry seeding. Live V1 addresses are copied into V2's key-based registry: the rate provider (
RATE_PROVIDER_KEY), funds holder (FUNDS_HOLDER_KEY), and oracle (ORACLE_KEY). The two V2-only keys are initialized from the call arguments —INSTANT_REDEEM_EXITPOINT_KEYandUPGRADE_MODULE_KEY— andFREEZE_REGISTRY_KEYis set fromfreezeRegistry(address(0)keeps freezing local-only). Only the live V1 addresses are copied; any pending V1 address-update proposal (proposedFundsHolder/proposedOracle/proposedRateProvider) is intentionally not migrated — a queued rotation is dropped, so resolve or record it before migrating (see Step-by-step upgrade). - High-water-mark rescale. V2 accounting lives in a single 1e18 share-price space. The stored high-water mark is rescaled out of V1's
10**decimals()share-value units withhighwaterMark *= 10 ** (18 - decimals())so it compares correctly against the 1e18-scaled oracle price. - Leaves the vault paused. Migration ends with the vault paused so operators can verify migrated addresses and price before reopening.
The 1e18 move is also reflected in the renamed in-memory accounting struct Totals: V1's totalSupply/shareValue fields are now totalShares/sharePrice. The deprecated V1 slots (_fundsHolder, _oracle, _proposedFundsHolder, _proposedOracle, _rateProvider, _proposedRateProvider) remain in the storage layout for compatibility but are superseded by the registry.
The original V1 contracts are preserved verbatim under src/legacy/v1/ (e.g. UltraVaultV1.sol, UltraVaultOracleV1.sol, IPriceSourceV1.sol). They are kept for reference and for the V1 compatibility shim (UltraVaultOracle.getQuote); they are not part of the live V2 deployment.
- Oracle API rebuilt.
setPrice,scheduleLinearPriceUpdate, andgetCurrentPriceare gone. Prices are pushed throughVaultPriceManager.instantPriceUpdate/linearPriceUpdate, which callUltraVaultOracle.applyPriceUpdate; read the live price withcurrentSharePrice(). See Oracles. - Consolidated redeem request. All V1
requestRedeemoverloads and…OfAssetvariants collapse intorequestRedeem(address asset, uint256 shares, address controller, address owner, bool autoClaim). - No single-asset fulfillment. V1
fulfillRedeemis removed; operators settle withfulfillMultipleRedeems(address[] assets, uint256[] shares, address[] controllers). accruedFees()return shape.accruedManagementFees()/accruedPerformanceFees()are replaced byaccruedFees()returning(uint256 managementFee, uint256 performanceFee).Types.solimport paths.Adjustment,Fees,Price,PendingRedeem, andClaimableRedeemmoved intosrc/interfaces/Types.sol; old per-interface import paths no longer resolve.Feesalso gainedinstantRedeemPremium.- Removed contracts.
UltraFeederand theOracleAdmincontract (andORACLE_ADMIN_ROLE) are gone; oracle operation is absorbed into theVaultPriceManageradmin model (isAdmin/onlyAdminOrOwner). - Address management. Per-field funds-holder / oracle / rate-provider propose-accept methods are replaced by the generic key-based
proposeAddressUpdate/acceptAddressUpdate.
- Deploy V2 supporting contracts. The concrete vault implementation (
CompliantUltraVaultorAllowlistUltraVault), the externalupgradeModule(timelocked upgrade authority), the V2 oracle/VaultPriceManager, and — if used — theinstantRedeemExitpointandfreezeRegistry. - Drain queued V1 governance first.
initializeV2carries over only the live V1 addresses; any outstanding V1 address-update proposal (proposedFundsHolder/proposedOracle/proposedRateProvider) is dropped, not migrated — a silent state loss. Before swapping, either accept or cancel every pending V1 proposal, or record it and replay it after migration viaproposeAddressUpdate/acceptAddressUpdate. - Swap the implementation and migrate — atomically. Point each live V1 proxy at the V2 implementation using the existing V1 upgrade authority, bundling the migration into the same call:
upgradeToAndCall(v2impl, abi.encodeCall(initializeV2, (instantRedeemExitpoint, upgradeModule, freezeRegistry))). BecauseinitializeV2is gated only byNotMigratablewith no caller-role check, it must execute in the same transaction as the swap — a swap that leaves it uncalled can be front-run by anyone supplying a hostileupgradeModule, seizing UUPS upgrade authority over the still-funded proxy. The call seeds the registry, rescales the high-water mark, and leaves the vault paused. - Wire pricing and upgrades. Hand the oracle to its
VaultPriceManager(claimOracleOwnership), configure CCIP destinations for cross-chain vaults, and confirm UUPS authorization now routes throughupgradeModule(). Switch to a new oracle, if desired, viaproposeAddressUpdate(ORACLE_KEY, …). - Verify, then reopen. Check the migrated registry addresses and the live
currentSharePrice(), then unpause withUNPAUSER_ROLE.