Yield Groups
A YieldGroup is a Source implementation: it aggregates one or more resources of a single protocol family behind the uniform IYieldGroupBase boundary the Hub depends on. Each YieldGroup is deployed per asset as a beacon proxy and owns its own inner deposit / withdraw queues, per-resource registry, and per-resource pause flags.
There are two YieldGroup contracts, deployed as three families in v1:
Core — the generic
YieldGroupcontract behind the Core beacon, registering Venus Core-pool vTokens (mint/redeemUnderlying) viaAdapterCoreV1, initialised with the chain'sblocksPerYear.Flux — the same
YieldGroupcontract behind the Flux beacon, registering Fluid Lending fTokens (ERC-4626 shares) viaAdapterFlux, initialised withblocksPerYear = 0.YieldGroupFRV— a separate contract, for Venus Fixed-Rate Vaults (ERC-4626 with an 11-state lifecycle).
"Core" and "Flux" are deployment identities, not contract names — which adapter, resources, caps and blocksPerYear governance wires into the proxy is the only difference. There is no YieldGroupCore or YieldGroupFlux type to import.
All three share the same Hub-facing surface and the same registry / queue / pause admin surface; they differ only in the protocol-specific behavior delegated to their adapter and in a few family-specific rules noted below.
Terminology. The PRD calls this layer a Source; the code names the contract a YieldGroup and the Hub-facing interface
IYieldGroupBase. There is noISourcetype — "Source" survives in the Solidity only as deployment-artifact aliases (CoreSource_USDT,FluxSource_USDC,FRVSource_U). A registered resource is the PRD's Product / Vault.
Hub-facing surface (IYieldGroupBase)
Every YieldGroup implements IYieldGroupBase. The Hub depends only on these functions and never reaches past a Source into the underlying market. Mutating entry points (deposit / withdraw / depositResource / withdrawResource) are onlyHub-gated (NotHub otherwise) and nonReentrant.
deposit(uint256 amount)→uint256 deposited— pullamountfrom the caller and place it across resources via the inner deposit queue.withdraw(uint256 amount, address to)— pullamountfrom resources via the inner withdraw queue (idle-first) and deliver exactlyamounttoto, or revert.depositResource(address resource, uint256 amount)→uint256 deposited— deposit the fullamountinto one specific resource, bypassing the inner queue (for Operator reallocation). Reverts if that resource cannot accept exactlyamount.withdrawResource(address resource, uint256 amount, address to)— redeem exactlyamountfrom one specific resource (no idle-first, no cascade); pulling from a paused resource is permitted (wind-down).accrue()—onlyHub; the Hub pokes every registered Source before reading NAV so the management fee is charged on interest-current value. All three families override the base no-op, so it is unreachable in deployed code. Core and Flux share theYieldGrouploop, which callsIResourceAdapter.accrueon every registered resource — real work only for Core (AdapterCoreV1calls the vToken'saccrueInterest()), an empty body for Flux.YieldGroupFRVinstead pokes each vault'supdateVaultState(), advancing its lifecycle. Every poke is best-effort per resource: one that reverts is isolated and reported viaResourceAccrualFailedrather than bubbling.Views —
asset(),totalAssets(),maxDeposit(),maxWithdraw(),spotAPYBps().
See Interfaces for the full IYieldGroupBase contract and its conventions.
ResourceConfig
Each YieldGroup stores per-resource state. The struct is declared on IYieldGroupBase:
The public accessor resourceConfig(address resource) returns the three fields as separate values (bool registered, bool paused, address adapter), not as a struct.
The optional per-resource deposit cap (Core & Flux only) lives in a separate resourceCap mapping so it does not disturb the struct's single-slot packing.
Initialization
Called once via the proxy:
Core and Flux are the same YieldGroup contract behind different beacons, so they share one initializer; only the argument they pass for blocksPerYear differs. FRV is a separate contract with its own.
Core —
initialize(address hub_, address asset_, uint256 blocksPerYear_, address acm_). Core annualises per-block supply rates, so it is deployed with the chain'sblocksPerYear(70,080,000 on BNB Chain, at roughly 0.45s per block).Flux — the same
initialize(address hub_, address asset_, uint256 blocksPerYear_, address acm_), deployed withblocksPerYear_ = 0:AdapterFluxreads a pre-annualised APR from the FluidLendingResolverand ignores the argument.FRV —
initialize(address hub_, address asset_, address acm_).YieldGroupFRVpasses0through to the shared base internally.
Each validates asset_ == Hub.asset() (HubAssetMismatch otherwise).
Registry (governance)
addResource(address resource, address adapter)— register a resource alongside the stateless adapter that handles its ABI. Validates that both are contracts (ResourceNotContract/AdapterNotContract) and thatadapter.asset(resource)matches the YieldGroup's underlying (ResourceAssetMismatch). It then callsadapter.validateRegistration(resource), which is where family-specific preconditions live:AdapterCoreV1rejects a vToken whose Comptroller charges a non-zerotreasuryPercent, whileAdapterFluxandAdapterFRVimplement it as a no-op. Does not auto-append to either inner queue. RevertsResourceAlreadyRegisteredif present. EmitsResourceAdded.removeResource(address resource)— remove a resource. Requires a zero raw receipt-token balance (ResourceHasBalanceotherwise) — the share-based check prevents a sub-unit residual from being rounded to zero and orphaning tokens. Also removes it from both inner queues via swap-and-pop, and clears its per-resource cap:addResourcenever writes that mapping and the cap setters are registered-only, so leaving it set would let a later re-add of the same address silently inherit a stale value. A remove / re-add cycle therefore starts from unbounded, and the cap must be set again. EmitsResourceRemoved.updateResourceAdapter(address resource, address adapter)— repoint a registered resource at a different adapter, without unwinding the position. Revalidates the new adapter the same wayaddResourcedoes. EmitsResourceAdapterUpdated.forceRemoveResource(address resource)— FRV only. Evict a resource whose vault has become unusable, abandoning any shares still held rather than blocking on the balance gate. RevertsResourceHasValueif the position is still worth recovering. EmitsResourceForceRemoved.
The same stateless adapter address may be reused across any number of resources in the same protocol family.
Other governance functions
setBlocksPerYear(uint256 blocksPerYear)— Core and Flux only. Retune the annualisation factor if the chain's block cadence changes. EmitsBlocksPerYearSet.sweep(address token, address to)— forward a stray ERC-20 balance out of the YieldGroup. Cannot take the underlying (SweepProtectedAsset) or any registered resource's receipt token (SweepProtectedResource).
Inner queues (Operator)
setInnerDepositQueue(address[] queue)— replace the inner deposit-routing order. Every entry must be a registered resource; duplicates revertInvalidQueue. EmitsInnerDepositQueueSet.setInnerWithdrawQueue(address[] queue)— replace the inner withdraw-routing order, independent of the deposit queue, under the same registration / duplicate validation; additionally, dropping a resource that still holds a receipt-token balance revertsWithdrawQueueOmitsFundedResource. The deposit queue carries no such guard — only the withdraw side must stay able to reach funded resources. EmitsInnerWithdrawQueueSet.
Pause (asymmetric)
pauseResource(address resource)— make the inner queue skip a resource. Operator-accessible. Balance still counts;depositResourceinto it reverts;withdrawResourcefrom it is allowed. EmitsResourcePauseToggled.unpauseResource(address resource)— governance-only. EmitsResourcePauseToggled.
There is no global YieldGroup pause — the only granularity inside a YieldGroup is per-resource. (A whole YieldGroup is paused at the Hub level via pauseYieldGroup.)
Resource caps (Core & Flux only)
An optional per-resource deposit cap limits how much underlying this YieldGroup holds in one market, independent of the market's own protocol supply cap. Effective room is min(protocolHeadroom, resourceCap − ourBalance).
raiseResourceCap(address resource, uint256 newCap)— loosen (Operator-accessible, likelowerResourceCap, so the Operator can open headroom ahead of a rebalance without a governance round).newCap == 0means unbounded, so raising to0removes the cap; the new value must be strictly looser (NotIncreasingotherwise). EmitsResourceCapRaised.lowerResourceCap(address resource, uint256 newCap)— tighten (Operator-accessible). Must be strictly tighter and non-zero (NotDecreasingotherwise). Lowering below the current balance simply stops new deposits; it never forces a withdrawal. EmitsResourceCapLowered.
YieldGroupFRV does not expose these — FRV capacity comes entirely from the vault's own cap and its Fundraising-only rule.
Family-specific behavior
Core (YieldGroup)
Flux (YieldGroup)
YieldGroupFRV
Resource
Venus Core vToken
Fluid fToken
Fixed-Rate Vault share
Redeem-time pool fee
grossed-up for Comptroller treasuryPercent if enabled; registration blocked while non-zero
none
none
Spot APY source
supplyRatePerBlock × blocksPerYear
Fluid LendingResolver (pre-annualised)
vault fixedAPY (Fundraising / Lock only)
Per-resource deposit cap
optional
optional (primary control — Fluid maxDeposit is effectively unbounded)
not applicable
Lifecycle
none
none
11-state machine (below)
FRV lifecycle
FRV vaults are ERC-4626 with an 11-state machine on top. YieldGroupFRV calls the vault's permissionless updateVaultState() before every mutating sizing read — both cascade legs and both targeted *Resource legs — so deposit / withdraw routing always acts on current state. Its accrue() override pokes every registered vault the same way, so any Hub operation that accrues fees advances the whole family's lifecycle, not only the vault being routed to. The maxDeposit() / maxWithdraw() views are view and structurally cannot advance state, so they can report a stale figure until someone pokes updateVaultState().
The full FRVVaultState enum (values 0–10): WaitingForMargin, MarginDeposited, Fundraising, InstitutionConfirmation, Lock, PendingSettlement, SettlementDeadlineExceeded, Matured, Failed, Liquidated, Closed.
Deposits are accepted only in
Fundraising(maxDepositis 0 in every other state). Each vault has aminSupplierDepositfloor: a sub-floor cascade leg is skipped to the next vault; a sub-floor targeteddepositResourcerevertsResourceBelowMinimumDeposit— unless the sub-floor amount exactly fills the vault's remaining capacity (the residual tail), which is always accepted.Withdrawals are possible only in the terminal states
Matured,Failed, orLiquidated— capital is locked throughLockandPendingSettlement.Maturedadds the fixed-rate yield;Failed/Liquidatedcan return less than principal, which marks down bothmaxWithdrawandtotalAssets.Mark-to-model gap.
AdapterFRVvalues a locked position at principal plus the coupon accrued straight-line over the lock, and holds the full term coupon flat throughPendingSettlementandSettlementDeadlineExceeded— whilemaxWithdrawstays0throughout. That accrued coupon therefore entersHub.totalAssets()and the ERC-4626 share price before it is realized or withdrawable, and it is written down only if the vault settlesFailed/Liquidated. Depositors minting during a lock buy in at a price that includes it; redeemers cannot exit against it until a terminal state.Because
maxDeposit()is a view it cannot advance state, so it can report stale non-zero capacity for a vault that is time-due to leaveFundraising; a permissionlessupdateVaultState()poke resolves it. In practice ordinary Hub traffic supplies that poke — everydeposit/mint/withdraw/redeem/reallocate/accrueFeesrunsYieldGroupFRV.accrue(), which callsupdateVaultState()on every registered vault, not just the one being routed to. This is a documented honesty-contract caveat, not a fund-safety issue.
Events
All three YieldGroups emit the shared set below. FRV omits the three IYieldGroup-only events (ResourceCapRaised, ResourceCapLowered, BlocksPerYearSet) and is the only one that emits ResourceForceRemoved:
ResourceAdded
resource, adapter
Resource registered alongside its adapter
ResourceRemoved
resource
Resource removed
ResourcePauseToggled
resource, paused
Resource pause flag flipped
InnerDepositQueueSet
queue
Inner deposit-routing order replaced
InnerWithdrawQueueSet
queue
Inner withdraw-routing order replaced
DepositRouted
resource, amount
Underlying placed into a resource
WithdrawRouted
resource, amount
Underlying pulled from a resource
ResourceCapRaised
resource, newCap
Per-resource deposit cap loosened (Core / Flux)
ResourceCapLowered
resource, newCap
Per-resource deposit cap tightened (Core / Flux)
ResourceAdapterUpdated
resource, oldAdapter, newAdapter
A resource's adapter was swapped
ResourceSkipped
resource, isDeposit
A cascade leg's adapter dispatch reverted and was routed around; a healthy system emits this zero times (paused / at-cap / dry resources are skipped silently, with no event)
ResourceAccrualFailed
resource, adapter
An adapter's accrual call reverted and was isolated
BlocksPerYearSet
oldBlocksPerYear, newBlocksPerYear
Annualisation factor retuned (Core / Flux)
ResourceForceRemoved
resource, orphanedShares
Bricked resource evicted, abandoning its shares (FRV)
Swept
token, to, amount
Stray-token balance rescued
Errors
NotHub
An onlyHub function was called by a non-Hub address
Unauthorized
Caller lacked the ACM role for the called function
ZeroAddress
A required non-zero address parameter was zero
HubAssetMismatch
The asset at init does not match the Hub's asset()
ResourceAssetMismatch
A resource's underlying does not match this YieldGroup's asset
ResourceAlreadyRegistered
addResource on an already-registered resource
ResourceNotRegistered
Operation targeted a resource not in the registry
ResourceHasBalance
removeResource while the resource still holds a balance
ResourceIsPaused
A deposit was routed to a paused resource
ResourceCapacityExceeded
A targeted depositResource exceeded that one resource's spare capacity. The inner-queue deposit path never reverts on capacity — it partial-fills and refunds the remainder
ResourceLiquidityInsufficient
Withdraw exceeded aggregate liquid funds across the inner queue
ResourceNotContract
The supplied resource address has no code
AdapterNotContract
The supplied adapter address has no code
AdapterUnderfilled
An adapter delegatecall's observed effect did not match the request
InvalidQueue
A queue contains a duplicate entry (an unregistered entry reverts ResourceNotRegistered instead)
NotIncreasing / NotDecreasing
A raiseResourceCap / lowerResourceCap call was not strictly looser / tighter (Core / Flux)
ResourceBelowMinimumDeposit
An FRV targeted deposit was below minSupplierDeposit and not the residual tail (FRV only)
ResourceHasValue
A force-removal target still holds value that would be abandoned
SweepProtectedAsset
sweep called with the YieldGroup's own underlying
SweepProtectedResource
sweep called with a registered resource's receipt token
WithdrawQueueOmitsFundedResource
An inner withdraw-queue replacement drops a funded resource
OnlySelf
An internal dispatch entry point was called externally
ACM role strings
Per YieldGroup, the role is keccak256(yieldGroupAddress, roleString).
Shared by all three families (8):
addResource(address,address), removeResource(address), updateResourceAdapter(address,address), setInnerDepositQueue(address[]), setInnerWithdrawQueue(address[]), pauseResource(address), unpauseResource(address), sweep(address,address).
Core and Flux add three, for 11 in total:
raiseResourceCap(address,uint256), lowerResourceCap(address,uint256), setBlocksPerYear(uint256).
FRV adds one instead, for 9 in total:
forceRemoveResource(address).
Last updated

