Hub
The Hub is the per-asset ERC-4626 entry point of the Liquidity Hub. It accepts a single underlying asset, mints share tokens, and routes capital through a registry of Sources under a governance-set policy. One Hub is deployed per asset as a beacon proxy.
A Hub extends ERC-4626 with a Source registry, dual caps, an asymmetric multi-level pause, Operator reallocation, and management, performance and redeem fees. Registry and queue admin logic is delegatecall-linked into the HubAdminLib library to keep the Hub under the 24 KB bytecode limit; the library executes in the Hub's storage context.
Inheritance
The Hub is built on the Venus upgradeable stack:
ERC4626Upgradeable— OpenZeppelin's ERC-4626 base (deposit / mint / withdraw / redeem, share accounting).AccessControlledV8— every privileged setter is gated throughAccessControlManagerV8via_checkAccessAllowed("<function signature>"). The role string is the literal signature text (e.g."addYieldGroup(address,uint256,uint16)"), never the 4-byte selector. This base also brings inOwnable2StepUpgradeable, which supplies the ownership surface documented below.ReentrancyGuardUpgradeable— supplies thenonReentrantguard on the asset-moving entry points (see Invariants and safety).HubStorage— isolates all Hub state in a base contract with a reserved__gapfor upgrade safety.IHub— external interface (see Interfaces for the boundary types it shares with YieldGroups).
The declaration order is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeable, HubStorage, IHub and is load-bearing for upgrade safety — never reorder it.
Storage lives entirely in HubStorage; never reorder or prepend its fields. New fields are appended and the __gap is shrunk by the same number of slots.
Operation flows
All user-facing mutating operations are atomic-or-revert — they complete in full or revert with a named error. There is no partial fill and no remainder returned.
Deposit
User calls
deposit(assets, receiver)(ormint).The Hub accrues fees, then pulls
assetsfrom the user — so the caller trades against the post-accrual share price.The Hub snapshots
totalAssets()(which already includes the just-transferred idle) and walks the outer deposit queue.For each Source: skip if unregistered or paused; compute
capRoom = max(0, effectiveCap − source.totalAssets()); sendmin(remaining, capRoom, source.maxDeposit()). A partial fill is accepted here — the Source places what its inner queue can absorb, refunds the unplaced remainder to the Hub, and returns the placed amount; the cascade routes that remainder to the next Source. A Source whosedepositcall reverts outright is caught, skipped (YieldGroupSkipped), and routing continues.YieldGroupUnderfilledis areallocate-only error, not a deposit-cascade one.Each Source walks its inner deposit queue, delegatecalling its adapter per resource (respecting any per-resource cap), and mints receipt tokens into the YieldGroup.
The Hub mints share tokens to the receiver.
If capacity across the whole queue is short, the entire transaction reverts (
HubCapacityExceeded).
Withdrawal
User calls
withdraw(assets, receiver, owner)(orredeem).The Hub accrues fees, enforces the per-transaction withdrawal cap (
HubWithdrawCapExceeded), then burns shares fromowner.The Hub consumes its own idle balance first, then walks the outer withdraw queue (independent of the deposit queue).
For each Source, the Hub calls
Source.withdraw(amount, hub); the Source uses its own idle balance first, then walks its inner withdraw queue, delegatecalling the adapter'sredeemUnderlying/withdraw.The Hub measures the delivered balance delta (rather than trusting a return value) and reverts
YieldGroupUnderfilledon any shortfall.Underlying flows Resource → YieldGroup → Hub → receiver.
If liquidity is short, the entire transaction reverts (
HubInsufficientLiquidity).
Reallocate
Reallocate moves assets between Sources — and, optionally, between specific resources within a Source — without funds entering or leaving the Hub. Both legs share the ReallocateLeg struct. The Hub treats resource as an opaque pass-through: it holds no resource registry and never reads resource state — it relays the address to the owning Source, which validates it against its own registry.
The Operator calls
reallocate(withdraws, deposits).The Hub accrues fees (skipped while paused).
Pull phase — every withdraw leg runs first:
Source.withdraw(queue) orSource.withdrawResource(targeted). Underlying returns to the Hub as idle. Pulling from a paused resource is allowed (wind-down). Every leg on both sides must carry a non-zeroamount— a zero-amount leg revertsZeroAmountrather than being skipped, which matters when the plan is generated programmatically and a no-op leg is a natural artifact.The Hub takes a single TVL snapshot after all pulls — the cap reference for every push (valid because a balanced reallocate conserves TVL). While unpaused this is the strict, fail-closed
totalAssets(). While paused (theemergencyReallocatepath) it is a fail-open sum that skips any Source whosetotalAssets()reverts, so a single bricked Source cannot block a rebalance among the healthy ones.Push phase — each deposit leg checks the Source is registered, unpaused, and within its effective cap, then
Source.deposit(queue) orSource.depositResource(targeted). Depositing into a paused resource reverts.The Hub enforces
Σ withdraws == Σ deposits(ReallocateImbalancedotherwise). Net-zero invariant.
A targeted leg (non-zero resource) moves the full amount into / out of that one resource or reverts. Setting both legs to the same source performs an intra-Source move (e.g. vUSDT_v1 → vUSDT_v2). emergencyReallocate performs the same net-zero rebalance but remains callable while the Hub is paused, giving governance a wind-down lever without granting the Operator a pause bypass.
totalAssets computation
totalAssets() drives share pricing for every deposit / withdraw (convertToShares / convertToAssets).
totalAssets()fails closed. It sums every registered Source with no error isolation, so a Source whosetotalAssets()view reverts makes the whole NAV read revert — and with it everyconvertTo*/preview*view and every deposit / mint / withdraw / redeem /accrueFees/reallocate— until the Source is fixed or evicted. This is deliberate: NAV is the share-price denominator, so silently counting a live Source as 0 would under-pay redeemers and over-credit depositors. Fault isolation applies only one level out, to the quantity-gate views (maxDeposit/maxWithdraw) and the deposit / withdraw routing, which do skip a faulting Source. Recovery is the emergency path ofremoveYieldGroup, which permits removal precisely when the view reverts.Hub idle is normally zero (deposits route out atomically, withdrawals pull exact amounts, reallocate is balanced). A direct token donation persists and is intentionally counted into NAV — it accrues to LPs and is consumed first on withdraw; the underlying cannot be swept.
View paths use the stored exchange rate (
exchangeRateStored, stale up to one accrual cycle, no state mutation). Mutating paths trigger interest accrual on the resource during mint / redeem, so the rate is current by the time the operation completes.
Cap enforcement
Source level — dual cap. Each Source carries an absolute amount AND a percentage of Hub TVL; the stricter binds:
percentageCapBps == 10_000 (100%) is a sentinel that disables the percentage component — required so a fresh Hub at TVL = 0 can take its first deposit (otherwise pct × 0 collapses the cap to zero). absoluteCap == type(uint256).max is rejected; use the sentinel to disable the percentage dimension instead.
Per-transaction withdrawal cap. maxWithdrawalSize (asset units) bounds every single withdraw / redeem so one transaction cannot drain a downstream product's liquidity; exceeding it reverts HubWithdrawCapExceeded.
The optional per-resource deposit cap (Core & Flux) binds one level down, inside the YieldGroup — see Yield Groups.
Fees
Three fee types. Management and performance fees are minted as dilution shares to a single feeRecipient (address(0) disables minting); the redeem fee is taken from the withdrawing lender. Accrual is idempotent within a block and runs before every deposit / withdraw / reallocate. The management and performance rates are capped at MAX_FEE_BPS (50%); the redeem fee is capped at MAX_REDEEM_FEE_BPS (5%).
Management fee — linear time proration:
totalAssets × bps × Δt / (10_000 × 365 days). A single accrual is capped atMAX_MGMT_FEE_PER_ACCRUAL_BPS(100%) of TVL. When the cap binds, the accrual cursor advances only over the window that produced the cap, so the remaining elapsed time defers to the next accrual — but the clamped fee itself is not minted: a separatetotalFee < totalAssets()guard skips any accrual whose combined management + performance fee would reach 100% of TVL. At the launch rate of0neither path is reachable.Performance fee — charged only on gains in price-per-share above a high-water mark (
totalAssets × 1e18 / totalSupply). The mark moves on three paths: it ratchets up to PPS on a new high during accrual (independent of whether a fee is minted, so a later rate increase cannot retroactively claim past gains); it is incremented by the retained exit-fee PPS uplift onwithdraw/redeem, so the redeem fee is not taxed as performance; and it is re-anchored to the entry PPS — possibly downward — on a refill from an empty vault, so a new cohort is neither shielded by nor charged for a prior cohort's high. It is therefore non-decreasing for any continuously-held cohort, but not monotonic across a full emptying.Redeem fee — an exit fee in BPS applied to
withdraw/redeem, charged directly to the withdrawing lender rather than diluting the pool. Capped atMAX_REDEEM_FEE_BPS(500= 5%).
v1 launches with all three set to 0; the machinery exists for governance to enable them later.
Multi-level pause
Three independent scopes — a broader scope blocks everything beneath it; siblings keep operating. Pause is asymmetric: tightening (pause) is Operator-accessible, loosening (unpause) is governance-only.
Hub paused — all deposits / withdrawals / mints / redeems,
reallocate, and fee accrual and the fee setters are blocked;emergencyReallocateandsweepstay callable; views stay readable, though all fourmax*views return0. Underlying products keep operating. Only the time-based management fee is excluded from the pause window —unpauseHubadvances the accrual cursor by the pause duration, so LPs are not charged rent for frozen time. The performance fee is not excluded: the high-water mark is deliberately left unchanged, so per-share gains the underlying products earn during the freeze are charged on the first post-resume accrual.Source paused — the Hub-level flag makes routing skip the Source silently in both directions: a user
deposit/mintcascades to the next Source and a userwithdraw/redeempulls from elsewhere, with noYieldGroupPausedrevert (onlyHubCapacityExceeded/HubInsufficientLiquidityfire if the rest of the queue cannot cover the amount). Its balance still counts intotalAssets()but is excluded frommaxDeposit()/maxWithdraw(). Funds remain reachable viareallocate/emergencyReallocate— a pull leg from a paused Source is allowed, while a push leg into one revertsYieldGroupPaused.Resource paused — set on the YieldGroup, not the Hub; see Yield Groups.
Permissions
Every ACM-gated call is authorised through AccessControlManagerV8 — the role is keccak256(abi.encodePacked(targetContract, roleString)), where roleString is the literal function signature. The ownership functions (transferOwnership / acceptOwnership / setAccessControlManager) sit outside the ACM and are owner-only; see Ownership and access control. The contracts do not hard-code an Operator-vs-governance branch; the asymmetry is entirely a matter of which addresses governance grants each role to.
Three holders in v1: governance (a VIP, behind the Normal Timelock), the Operator (a Venus operations multisig acting as the routine keeper, distinct from the Guardian), and the Guardian (a multisig that acts with no timelock delay, so it can contain an incident immediately). The Guardian is granted containment only — it can tighten but never loosen, and holds no unpause, so it can never undo a governance-ordered pause. Both role holders are addresses granted by the onboarding VIP, not roles baked into the bytecode; the granted addresses are listed under Deployment.
Add / remove YieldGroup, add / remove / re-adapter a resource
✅
❌
❌
Set fees and fee recipient, sweep, setBlocksPerYear
✅
❌
❌
Raise the per-transaction withdrawal cap
✅
❌
❌
Unpause at any level (Hub, YieldGroup, resource)
✅
❌
❌
Raise or lower YieldGroup caps and per-resource caps
✅
✅
❌
Lower the per-transaction withdrawal cap, reorder outer / inner queues
✅
✅
❌
Pause — Hub, YieldGroup, or resource
✅
✅
✅
emergencyReallocate (works while paused)
✅
❌
✅
forceRemoveResource (FRV)
✅
❌
✅
reallocate between YieldGroups / resources
❌
✅
❌
addHub / removeHub on the HubRegistry
✅
❌
❌
deposit / mint / withdraw / redeem, accrueFees, views
permissionless
permissionless
permissionless
Two rows are worth reading carefully, because both depart from a naive loosening-vs-tightening split:
raiseYieldGroupCapis Operator-accessible. It is the one loosening lever the Operator holds, granted so it can open headroom immediately before areallocatewithout waiting for a governance round. Raising the per-transaction withdrawal cap is still governance-only.Governance does not hold
reallocate. It is Operator-only. Governance's equivalent lever isemergencyReallocate, which has the same net-zero semantics and additionally works while the Hub is paused.
The Guardian's grant is a strict subset of governance's: pause at all three levels, emergencyReallocate, and forceRemoveResource. It holds no unpause, so it can contain an incident but never undo a governance-ordered pause.
Nothing in the contracts hard-codes these three. The split is entirely a matter of which addresses governance grants each role to — the asymmetry is a deployment decision, not a code branch.
Invariants and safety
Atomic-or-revert. Deposits, withdrawals, and reallocate fully complete or revert with a named error — no partial fills, no remainder returned.
Net-zero reallocate.
Σ withdraws == Σ deposits; the Operator can only move funds among registered routes, never in or out.Reentrancy-guarded value paths. Every entry point that moves assets is
nonReentrant(ReentrancyGuardUpgradeable) —deposit/mint/withdraw/redeem,reallocate,emergencyReallocate,accrueFees,sweepon the Hub, anddeposit/withdraw/depositResource/withdrawResource/sweepon each YieldGroup — since each makes external calls into Sources, delegatecalls into adapters, and reaches into the underlying Core / Flux / FRV protocols. The ACM-gated admin setters and theonlyHubaccrue()poke are not individually guarded; they rely on ACM gating andonlyHubinstead.Fault isolation on the quantity gates, fail-closed on price. A Source with a reverting
totalAssets()view contributes 0 tomaxDeposit()/maxWithdraw()and is skipped by the deposit / withdraw routing — butHub.totalAssets()itself is deliberately fail-closed and reverts with it, halting share pricing rather than understating NAV. Such a Source can still be removed as an emergency eviction:removeYieldGroupcatches the reverting view and permits removal.Removal safety.
removeYieldGroupgates on the Source's balance so a funded Source can't be silently dropped; YieldGroups apply the same gate on a raw receipt-token balance.Inflation defense. The ERC-4626 decimals offset is set per asset at
initializeand enforced on-chain in both directions:0and anything aboveMAX_DECIMALS_OFFSET(12) revertInvalidDecimalsOffset, so a Hub can never be deployed with the offset disabled. A non-zero offset is required for every asset regardless of its decimals — offset 0 would let a donation attack zero a first depositor's shares.Standard ERC-20 only. Fee-on-transfer, deflationary, and rebasing underlyings are unsupported (deposits fail closed if a token delivers less than requested).
Upgrade-safe storage. State is isolated in
HubStoragewith a reserved__gap; fields are never reordered or prepended.
Constants
BPS_DENOMINATOR
10_000
Basis-point denominator (100%)
EXP_SCALE
1e18
Fixed-point mantissa
MAX_DECIMALS_OFFSET
12
Maximum ERC-4626 inflation-defense decimals offset
MAX_FEE_BPS
5_000
Maximum management or performance fee rate (50%)
MAX_MGMT_FEE_PER_ACCRUAL_BPS
10_000
Cap on one management-fee accrual (100% of TVL); remainder defers
MAX_REDEEM_FEE_BPS
500
Maximum redeem (exit) fee rate (5%)
SECONDS_PER_YEAR
365 days
Seconds per year, used for management-fee time proration
State variables
Defined in HubStorage:
_hubPaused
bool
True while the Hub is paused
_decimalsOffsetStored
uint8
ERC-4626 inflation-defense offset, set once at init
_maxWithdrawalSize
uint256
Per-transaction withdraw / redeem cap, in asset units
_registeredYieldGroups
address[]
Canonical set of registered Sources
_yieldGroupIndex
mapping(address => uint256)
1-indexed position in the registry (0 = not registered)
_yieldGroups
mapping(address => YieldGroupConfig)
Per-Source caps, pause, and registered flag
_outerDepositQueue
address[]
Deposit cascading order
_outerWithdrawQueue
address[]
Withdraw pulling order (independent of deposit queue)
_managementFeeBps
uint16
Management fee rate in BPS
_performanceFeeBps
uint16
Performance fee rate in BPS
_feeRecipient
address
Recipient of newly-minted fee shares
_redeemFeeBps
uint16
Redeem (exit) fee rate in BPS; retained in the vault as a price-per-share uplift, not paid to _feeRecipient
_highWaterMarkPerShare
uint256
Performance HWM (totalAssets × 1e18 / totalSupply)
_lastFeeAccrualTimestamp
uint64
Timestamp of last fee accrual
_pauseStart
uint64
Timestamp the current pause began; management-fee accrual skips the window
_lastFeeAccrualBlock
uint64
Block of last fee accrual; makes accrual idempotent per block
The rows are listed in HubStorage declaration order. _managementFeeBps, _performanceFeeBps, _feeRecipient and _redeemFeeBps share one packed slot.
Structs
YieldGroupConfig
ReallocateLeg
The same shape is reused for both the withdraw (pull) and deposit (push) arrays of reallocate. resource == address(0) cascades through the Source's inner queue (idle-first on a pull, capacity-order on a push); a non-zero resource moves the full amount against that one market / vault and bypasses the queue.
Solidity API
ERC-4626 user functions
Permissionless and atomic-or-revert. Each accrues fees before pricing.
deposit(uint256 assets, address receiver)— depositassets, route through the outer deposit queue, mint shares toreceiver. RevertsHubCapacityExceeded/HubPaused.mint(uint256 shares, address receiver)— mint exactlyshares, depositing the required assets.withdraw(uint256 assets, address receiver, address owner)— burn shares fromowner, pullassets(Hub idle first, then the outer withdraw queue), deliver toreceiver. Size the call againstmaxWithdraw(owner): it is already clamped to the owner's balance, aggregate liquidity andmaxWithdrawalSize(), so an over-sized request trips the inherited OpenZeppelin guard and reverts with the string error"ERC4626: withdraw more than max"rather thanHubWithdrawCapExceededorHubInsufficientLiquidity. Those two named errors remain reachable on the routing path.redeem(uint256 shares, address receiver, address owner)— burn exactlysharesand deliver the corresponding assets.totalAssets()—Σ Source.totalAssets() + Hub idle balance.
Two consent-gated variants take an extra bytes32 consentHash and emit it in the same transaction as the deposit, so an integrator can prove the supplier acknowledged a specific set of terms. The hash is event-only — no storage is written and there is no on-chain getter, so proving acknowledgement is a log query, not a contract read. They are otherwise identical to their plain counterparts.
depositWithConsent(uint256 assets, address receiver, bytes32 consentHash)— emitsConsentRecordedwhenconsentHashis non-zero, then deposits. Passingbytes32(0)skips the emit and behaves exactly likedeposit.mintWithConsent(uint256 shares, address receiver, bytes32 consentHash)— the same, formint.
Not all the standard ERC-4626 views behave identically to the OpenZeppelin base:
convertToShares/convertToAssets/previewDeposit/previewMintare inherited unmodified. They are pure share math and reflect none of the caps, liquidity, pause state or fees — sizing a deposit offpreviewDepositgives no signal that the deposit will revert.maxDeposit/maxMint/maxWithdraw/maxRedeemare overridden and do reflect live effective caps, aggregate liquidity, the per-transaction withdrawal cap and the redeem fee. All four return0while the Hub is paused.previewWithdraw/previewRedeemare overridden to account for the redeem (exit) fee, so they diverge fromconvertToAssets/convertToShareswhenever that fee is non-zero.
Source registry (governance)
addYieldGroup(address source, uint256 absoluteCap, uint16 percentageCapBps)— register a Source. Validatessource.asset() == asset()(YieldGroupAssetMismatch) and the cap pair (InvalidCap);YieldGroupAlreadyRegisteredif present. EmitsYieldGroupAdded.removeYieldGroup(address source)— remove a Source.YieldGroupHasBalanceif it still custodies a balance. EmitsYieldGroupRemoved.raiseYieldGroupCap(address source, uint256 absoluteCap, uint16 percentageCapBps)— loosen caps. Both dimensions must be ≥ their current values and at least one must strictly increase; a full no-op revertsNotIncreasing. Raising one dimension while lowering the other is rejected — that needs two calls. Also revertsYieldGroupNotRegisteredandInvalidCap. Operator-accessible. EmitsYieldGroupCapRaised.lowerYieldGroupCap(address source, uint256 absoluteCap, uint16 percentageCapBps)— tighten caps. Both dimensions must be ≤ their current values and at least one must strictly decrease; a full no-op revertsNotDecreasing. Also revertsYieldGroupNotRegisteredandInvalidCap. Operator-accessible. EmitsYieldGroupCapLowered.
Outer queues (Operator)
setOuterDepositQueue(address[] queue)— replace the deposit routing order; every entry must be a registered Source (YieldGroupNotRegisteredotherwise) with no duplicates (InvalidQueue). EmitsOuterDepositQueueSet.setOuterWithdrawQueue(address[] queue)— replace the withdraw routing order under the same registration / duplicate validation; additionally, dropping a Source that still holds a balance revertsWithdrawQueueOmitsFundedYieldGroup. The funded test is fail-closed: a Source whosetotalAssets()view reverts counts as funded, so a bricked Source can never be dropped from the withdraw queue — the mirror ofremoveYieldGroup, which deliberately permits evicting it from the registry in exactly that case. Note also thattotalAssets()counts a Source's idle balance, so a 1-wei donation to an otherwise-empty Source is enough to pin it in the queue. EmitsOuterWithdrawQueueSet.
Reallocate (Operator)
reallocate(ReallocateLeg[] withdraws, ReallocateLeg[] deposits)— atomic net-zero rebalance;ReallocateImbalancedif sums differ. EmitsWithdrawRouted/DepositRouted.emergencyReallocate(ReallocateLeg[] withdraws, ReallocateLeg[] deposits)— same semantics, callable while paused, governance-only role.
Pause (asymmetric)
pauseHub()/unpauseHub()— pause is Operator-accessible (also Guardian); unpause is governance-only. EmitsHubPauseToggled.pauseYieldGroup(address source)/unpauseYieldGroup(address source)— pause is Operator-accessible; unpause is governance-only. EmitsYieldGroupPauseToggled.
Per-transaction withdrawal cap
raiseMaxWithdrawalSize(uint256 newSize)— governance-only;NotIncreasingif not strictly higher. EmitsMaxWithdrawalSizeRaised.lowerMaxWithdrawalSize(uint256 newSize)— Operator-accessible;NotDecreasingif not strictly lower, andZeroAmountifnewSize == 0— the cap can never be set to zero, so usepauseHub()to stop withdrawals entirely. EmitsMaxWithdrawalSizeLowered.
Fees
setManagementFeeBps(uint16 bps)/setPerformanceFeeBps(uint16 bps)— set fee rates; pending fees accrue at the OLD rate first.InvalidFeeBpsaboveMAX_FEE_BPS. EmitsManagementFeeBpsSet/PerformanceFeeBpsSet.setFeeRecipient(address recipient)— set the recipient; pending fees mint to the OLD recipient first;address(0)disables minting. EmitsFeeRecipientSet.accrueFees()— permissionless poke; idempotent within a block. EmitsFeesAccrued/HighWaterMarkUpdated.setRedeemFeeBps(uint16 bps)— set the exit fee charged onwithdraw/redeem.InvalidFeeBpsaboveMAX_REDEEM_FEE_BPS. EmitsRedeemFeeBpsSet.
Sweep
sweep(address token, address to)— forward the Hub's full balance of an arbitrary ERC-20 toto.SweepProtectedAssetiftoken == asset()— the underlying can never be swept. Governance-only. EmitsSwept.
Ownership and access control
The Hub is Ownable2Step, so a transfer only completes when the incoming owner accepts it. Ownership is separate from the ACM roles above: the owner replaces the ACM itself, while the ACM decides who may call everything else. Both sit with governance in v1.
transferOwnership(address newOwner)— nominate a new owner. EmitsOwnershipTransferStarted.acceptOwnership()— the nominee claims it. EmitsOwnershipTransferred. The onboarding proposal calls this on each Hub.pendingOwner()/owner()— the nominee and the current owner.setAccessControlManager(address acm)— point the Hub at a differentAccessControlManagerV8. Owner-only. EmitsNewAccessControlManager.accessControlManager()— the ACM currently in force.
Views
yieldGroupConfig(address source)→YieldGroupConfig— full stored config for a Source.yieldGroupEffectiveCap(address source)→uint256— live effective cap (the stricter of the dual cap).registeredYieldGroups()→address[]— the Source registry.outerDepositQueue()/outerWithdrawQueue()→address[]— the current routing orders.maxWithdrawalSize()→uint256— per-transaction withdraw cap.hubPaused()→bool— Hub-level pause state.feeRecipient()→address— current fee recipient.feeBps()→(uint16 managementBps, uint16 performanceBps)— current dilution fee rates.redeemFeeBps()→uint16— current exit fee rate.highWaterMarkPerShare()→uint256— performance HWM in share-price units.
Events
YieldGroupAdded
yieldGroup (indexed), absoluteCap, percentageCapBps
Source registered
YieldGroupRemoved
yieldGroup (indexed)
Source removed
YieldGroupCapRaised
yieldGroup (indexed), absoluteCap, percentageCapBps
Source caps loosened
YieldGroupCapLowered
yieldGroup (indexed), absoluteCap, percentageCapBps
Source caps tightened
YieldGroupPauseToggled
yieldGroup (indexed), paused
Source pause flag flipped
OuterDepositQueueSet
queue
Deposit routing order replaced
OuterWithdrawQueueSet
queue
Withdraw routing order replaced
HubPauseToggled
paused
Hub pause flag flipped
MaxWithdrawalSizeRaised
oldSize, newSize
Per-tx withdraw cap raised
MaxWithdrawalSizeLowered
oldSize, newSize
Per-tx withdraw cap lowered
DepositRouted
yieldGroup (indexed), amount
Assets deposited into a Source
WithdrawRouted
yieldGroup (indexed), amount
Assets withdrawn from a Source
ManagementFeeBpsSet
oldBps, newBps
Management fee rate changed
PerformanceFeeBpsSet
oldBps, newBps
Performance fee rate changed
FeeRecipientSet
oldRecipient, newRecipient
Fee recipient changed
FeesAccrued
managementShares, performanceShares, totalShares
Fee shares minted in an accrual
HighWaterMarkUpdated
oldHwm, newHwm
Performance HWM changed — ratcheted up on a new PPS high, incremented by the retained exit-fee uplift on withdraw, or re-anchored (possibly downward) to the entry PPS on a refill from an empty vault. Do not assume newHwm >= oldHwm
RedeemFeeBpsSet
oldBps, newBps
Redeem (exit) fee rate changed
YieldGroupSkipped
yieldGroup, isDeposit
A YieldGroup's mutating deposit / withdraw call reverted and routing routed around it. A healthy system emits this zero times — paused, at-cap and dry YieldGroups are skipped silently with no event
ConsentRecorded
supplier, receiver, consentHash
A consent-gated deposit or mint recorded its hash
Swept
token, to, amount
Stray-token balance rescued
OwnershipTransferStarted
previousOwner, newOwner
Ownership transfer nominated
OwnershipTransferred
previousOwner, newOwner
Ownership transfer accepted
NewAccessControlManager
oldAccessControlManager, newAccessControlManager
The Hub was pointed at a different ACM
Errors
Unauthorized
Caller lacked the ACM role for the called function
HubCapacityExceeded
Deposit / mint exceeds aggregate spare cap across unpaused Sources, or a reallocate / emergencyReallocate push leg exceeds the destination Source's own effective cap
HubInsufficientLiquidity
Withdraw / redeem exceeds liquid funds (withdraw queue + Hub idle)
YieldGroupUnderfilled
A Source delivered an amount not exactly equal to the amount requested — under- or over-delivery, since the check is != — on a user withdraw / redeem cascade step or on a reallocate leg. Not raised by the deposit cascade, which accepts partial fills
HubWithdrawCapExceeded
Withdraw / redeem exceeds maxWithdrawalSize()
HubPaused
A user-facing mutating call is attempted while the Hub is paused
YieldGroupPaused
A reallocate / emergencyReallocate push leg targets a paused Source. User deposits never raise this — the cascade skips paused Sources silently and surfaces HubCapacityExceeded if the amount cannot be placed
YieldGroupNotRegistered
Operation targets a Source not in the registry
YieldGroupAlreadyRegistered
addYieldGroup on an already-registered Source
YieldGroupHasBalance
removeYieldGroup while the Source still holds non-zero resource-backed value; its idle asset() balance is netted out, so a Source holding only a dust donation is removable. If the Source's totalAssets() reverts, removal is permitted anyway as an emergency eviction
YieldGroupAssetMismatch
A Source's asset() does not match the Hub's underlying
InvalidCap
Cap pair is invalid (e.g. percentage > 100%, or absoluteCap == uint256.max)
InvalidQueue
A queue contains a duplicate entry (an unregistered entry reverts YieldGroupNotRegistered instead)
ReallocateImbalanced
reallocate withdraw and deposit sums do not match
WithdrawQueueOmitsFundedYieldGroup
A withdraw-queue replacement drops a funded Source. A Source whose totalAssets() reverts is treated as funded, so it cannot be dropped
SweepProtectedAsset
sweep called with token == asset()
ZeroAddress / ZeroAmount
A required non-zero address / amount parameter was zero. ZeroAmount also fires on a zero-amount reallocate / emergencyReallocate leg (either side), and on any attempt to set maxWithdrawalSize to 0
InvalidDecimalsOffset
decimalsOffset_ at init was 0 or exceeded MAX_DECIMALS_OFFSET (12)
InvalidFeeBps
A management / performance rate exceeded MAX_FEE_BPS (5000 = 50%), or the redeem rate exceeded MAX_REDEEM_FEE_BPS (500 = 5%)
NotIncreasing / NotDecreasing
raiseMaxWithdrawalSize / lowerMaxWithdrawalSize was not strictly increasing / decreasing. For raiseYieldGroupCap / lowerYieldGroupCap, both dimensions must move in the requested direction (or stay equal) and at least one must move strictly — mixing a raise of one with a lower of the other reverts
ACM role strings
The role gating each privileged function is keccak256(hubAddress, roleString) where roleString is the literal signature. The ACM binds a role per contract address, so each per-asset Hub carries its own copy of all 19.
addYieldGroup(address,uint256,uint16), removeYieldGroup(address), raiseYieldGroupCap(address,uint256,uint16), lowerYieldGroupCap(address,uint256,uint16), setOuterDepositQueue(address[]), setOuterWithdrawQueue(address[]), pauseHub(), unpauseHub(), pauseYieldGroup(address), unpauseYieldGroup(address), raiseMaxWithdrawalSize(uint256), lowerMaxWithdrawalSize(uint256), setManagementFeeBps(uint16), setPerformanceFeeBps(uint16), setFeeRecipient(address), setRedeemFeeBps(uint16), sweep(address,address), reallocate((address,address,uint256)[],(address,address,uint256)[]), emergencyReallocate((address,address,uint256)[],(address,address,uint256)[]).
Note the two ReallocateLeg[] signatures expand the struct to its tuple form (address,address,uint256)[] — the ACM role string must use the expanded form, not ReallocateLeg[].
The HubRegistry carries two of its own: addHub(address) and removeHub(address). Per-YieldGroup role strings are listed in Yield Groups.
Last updated

