Adapters
An adapter translates between a YieldGroup and one protocol-specific ABI. Each adapter is a stateless singleton: a single deployment per ABI family serves every YieldGroup that registers a matching resource — there is no per-(YieldGroup, resource) instance, no proxy, and no clone factory.
There are three adapters in v1:
AdapterCoreV1— Venus Core V1 vTokens (Compound-stylemint/redeemUnderlying).AdapterFlux— Fluid Lending fTokens (ERC-4626).AdapterFRV— Venus Fixed-Rate Vault shares (ERC-4626).
All three implement IResourceAdapter.
Dispatch model
The split between mutating and view dispatch is load-bearing for security:
Mutating functions (
deposit,withdraw) MUST be invoked viadelegatecallfrom the YieldGroup. They execute in the YieldGroup's storage context, so receipt-token credits and debits land on the YieldGroup, not the adapter. From the resource's perspective,msg.senderis the YieldGroup.View functions are invoked via normal
call/staticcall. Where the answer depends on whose position is being queried, they take an explicitholderparameter (the YieldGroup).
onlyDelegateCall guard
Each adapter captures its own address into an immutable (_ADAPTER_SELF) at construction. The onlyDelegateCall modifier reverts NotDelegateCall when address(this) == _ADAPTER_SELF — i.e. when a mutating function is invoked directly on the adapter rather than through a delegatecall. This prevents an accidental direct call that would orphan receipt tokens on the adapter. (Immutables live in bytecode, not storage slots, so they are safe to read across a delegatecall.)
Storage-safety invariants
Verified at audit for every adapter:
Zero storage variables — only
immutableandconstantvalues are declared; the contract neversstores.No inline assembly performs
sstore.No arbitrary call targets — external calls go only to the supplied
resourceand its trusted, chain-fixed dependencies, never a user-supplied address. (AdapterCoreV1calls the vToken'sunderlying()andcomptroller();AdapterFluxcalls the fToken'sasset()and the FluidLendingResolver;AdapterFRVcalls the vault'sasset().)Mutating functions revert outside a delegatecall context (the
onlyDelegateCallguard).
IResourceAdapter surface
deposit(address resource, uint256 amount)→uint256 deposited— depositamountof underlying intoresource; receipt tokens are credited to the YieldGroup.withdraw(address resource, uint256 amount, address to)— redeem exactlyamountof underlying fromresourceand deliver it toto.asset(address resource)→address— the underlying ERC-20 accepted byresource.totalAssets(address resource, address holder)→uint256— underlying valueholderholds viaresource. The basis is per-adapter:AdapterCoreV1uses the stale (non-accruing)exchangeRateStorednet of any ComptrollertreasuryPercent;AdapterFluxuses the fToken's livepreviewRedeem;AdapterFRVuses a time-based linear coupon accrual.maxDeposit(address resource)→uint256— spare deposit headroom onresourceright now.maxWithdraw(address resource, address holder)→uint256— underlyingholdercan withdraw right now, net of any redeem-time protocol fee.spotAPYBps(address resource, uint256 blocksPerYear)→uint64— spot supply-side APY in BPS.receiptBalance(address resource, address holder)→uint256— raw receipt-token balance (vToken / fToken / FRV shares), in receipt-token units — used byremoveResourceas a share-based emptiness gate.accrue(address resource)— settleresource's own global interest state so a followingtotalAssetsread prices the position at a fresh rate. Invoked via normalcall, not delegatecall, and therefore carries noonlyDelegateCallguard: it mutates the resource's global index, not the holder's position. Only block-lazy adapters do real work —AdapterCoreV1calls the vToken'saccrueInterest();AdapterFluxandAdapterFRVare no-ops.validateRegistration(address resource)— reverts ifresourcefails a protocol-specific registration precondition.
See Interfaces for the full IResourceAdapter contract and its pre/post-conditions.
AdapterCoreV1
Wraps Venus Core V1 vTokens.
deposit—forceApproves the vToken and callsmint(amount); revertsVTokenMintFailedon a non-zero Compound error code.withdraw— grosses up the request for the Comptroller's treasury cut (ceil-division), callsredeemUnderlying, verifies the received balance delta, and transfers exactlyamounttoto. Any gross-up surplus stays as idle on the YieldGroup (counted intotalAssets, consumed idle-first next time). RevertsVTokenRedeemFailed/VTokenUnderfilledon failure.Treasury-fee handling — when the Comptroller's
treasuryPercent != 0,redeemUnderlying(X)delivers onlyX − fee. The gross-up keeps individual redeems whole.validateRegistrationrejects a vToken whose Comptroller already charges a non-zerotreasuryPercent(TreasuryFeeUnsupported) — that exit fee leaves the pool without burning shares, so it is unmodeled in the Hub's gross NAV. The adapter performs no upper-bound check of its own ontreasuryPercent; it relies on the Venus Comptroller's setter enforcingtreasuryPercent < 1e18, which is what keeps theMANTISSA_ONE − treasuryPctsubtraction and the gross-up denominator safe. Were that invariant ever violated on-chain,withdrawandtotalAssetswould revert on arithmetic underflow rather than with a named error.Dust handling — a withdraw cascade can hand the adapter a sub-one-vToken redeem (e.g. a 1-wei remainder from an upstream ERC-4626 source) that Compound would floor to zero tokens and reject. In that case the adapter redeems exactly one vToken unit of underlying so the burn is non-zero, then transfers only
amountand leaves the surplus idle on the YieldGroup. A no-op for normal-sized redeems.Spot APY —
supplyRatePerBlock × blocksPerYear, scaled to BPS and clamped touint64.max.maxDeposit— honors the Comptroller's mint pause and supply cap (supplyCap == 0rejects mint outright, surfaced as 0 capacity), then trims a 0.1% conservative margin (raw − raw / 1000) off the remaining headroom, so a normal inter-block interest accrual between the view and the mint cannot pushdeposit(maxDeposit())over the cap. Expect a persistent small shortfall when reconciling against the Comptroller's rawsupplyCaps(vToken).vBNB is unsupported by design —
IVToken(resource).underlying()reverts on vBNB, which tripsasset()and prevents registration.
Constants: MANTISSA_ONE (1e18), EXP_SCALE (1e18), MANTISSA_TO_BPS (1e14).
Errors: NotDelegateCall, VTokenMintFailed, VTokenRedeemFailed, VTokenAccrueFailed, VTokenUnderfilled, TreasuryFeeUnsupported.
AdapterFlux
Wraps Fluid Lending fTokens (ERC-4626 shares). The adapter holds the Fluid LendingResolver address as an immutable, set at construction (ZeroResolver if zero).
deposit/withdraw— uses the fToken's ERC-4626deposit/withdraw. There is no redeem-time pool fee (Fluid's cut is already in the supply rate), so no gross-up is needed.Spot APY — read from the Fluid
LendingResolverviagetFTokenDetails, as the basesupplyRate(already in BPS) plusrewardsRate / 1e10(1e12-precision rewards converted to BPS), clamped touint64.max. Both are pre-annualised APRs, not per-block rates, so theblocksPerYearargument is unused here. Reconciling this figure against Fluid's basesupplyRatealone will show a gap equal to the reward rate.maxDeposit— a Fluid fToken's protocol-levelmaxDepositis effectively unbounded, which is why the per-resource cap on the Flux-configuredYieldGroupdeployment is the primary deposit control.validateRegistration— a no-op (pure); Flux has no per-redeem pool fee to guard against.
Errors: NotDelegateCall, ZeroResolver.
AdapterFRV
Wraps Venus Fixed-Rate Vault shares (ERC-4626).
deposit/withdraw— uses the vault's ERC-4626deposit/withdraw. There is no redeem-time pool fee (the reserve factor is taken once at settlement).spotAPYBps— reads the vault'sstate()and returns the vault'sfixedAPYonly whileFundraisingorLock; in any other state it contributes no APY (the rate is not meaningful outside the active window).State machine — the adapter does not call
updateVaultState();YieldGroupFRVpokes it before every routing decision so the adapter always reads a current state (see the FRV lifecycle).validateRegistration— a no-op (pure); FRV has no per-redeem pool fee.
Errors: NotDelegateCall.
Adding a new protocol family
Because adapters are stateless and reached only through IResourceAdapter, supporting a new yield protocol (e.g. a future Core V2) requires only a new adapter deployment plus per-YieldGroup addResource calls — no changes to the Hub, the YieldGroups, the interfaces, or any existing adapter.
Last updated

