@venusprotocol/liquidity-hub 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +422 -0
  3. package/contracts/Hub/Hub.sol +1614 -0
  4. package/contracts/Hub/HubStorage.sol +106 -0
  5. package/contracts/Hub/lib/HubAdminLib.sol +292 -0
  6. package/contracts/YieldGroup/YieldGroup.sol +128 -0
  7. package/contracts/YieldGroup/YieldGroupFRV.sol +107 -0
  8. package/contracts/YieldGroup/base/YieldGroupBase.sol +797 -0
  9. package/contracts/YieldGroup/base/YieldGroupBaseStorage.sol +73 -0
  10. package/contracts/adapters/AdapterCoreV1.sol +309 -0
  11. package/contracts/adapters/AdapterFRV.sol +276 -0
  12. package/contracts/adapters/AdapterFlux.sol +173 -0
  13. package/contracts/interfaces/IHub.sol +551 -0
  14. package/contracts/interfaces/IHubRegistry.sol +120 -0
  15. package/contracts/interfaces/IMigrator.sol +134 -0
  16. package/contracts/interfaces/IResourceAdapter.sol +154 -0
  17. package/contracts/interfaces/IYieldGroup.sol +119 -0
  18. package/contracts/interfaces/IYieldGroupBase.sol +507 -0
  19. package/contracts/interfaces/IYieldGroupFRV.sol +61 -0
  20. package/contracts/interfaces/external/IComptroller.sol +57 -0
  21. package/contracts/interfaces/external/IFRVVault.sol +113 -0
  22. package/contracts/interfaces/external/IFToken.sol +46 -0
  23. package/contracts/interfaces/external/IFluidLendingResolver.sol +63 -0
  24. package/contracts/interfaces/external/IVBep20.sol +39 -0
  25. package/contracts/interfaces/external/IVToken.sol +125 -0
  26. package/contracts/interfaces/external/IWBNB.sol +16 -0
  27. package/contracts/migrator/Migrator.sol +182 -0
  28. package/contracts/registry/HubRegistry.sol +179 -0
  29. package/deployments/bscmainnet/AdapterCoreV1.json +336 -0
  30. package/deployments/bscmainnet/AdapterFRV.json +225 -0
  31. package/deployments/bscmainnet/AdapterFlux.json +249 -0
  32. package/deployments/bscmainnet/CoreBeacon.json +110 -0
  33. package/deployments/bscmainnet/CoreSource_U.json +1083 -0
  34. package/deployments/bscmainnet/CoreSource_USDC.json +1083 -0
  35. package/deployments/bscmainnet/CoreSource_USDT.json +1083 -0
  36. package/deployments/bscmainnet/FRVBeacon.json +110 -0
  37. package/deployments/bscmainnet/FRVSource_U.json +978 -0
  38. package/deployments/bscmainnet/FRVSource_USDC.json +978 -0
  39. package/deployments/bscmainnet/FRVSource_USDT.json +978 -0
  40. package/deployments/bscmainnet/FluxBeacon.json +110 -0
  41. package/deployments/bscmainnet/FluxSource_U.json +1083 -0
  42. package/deployments/bscmainnet/FluxSource_USDC.json +1083 -0
  43. package/deployments/bscmainnet/FluxSource_USDT.json +1083 -0
  44. package/deployments/bscmainnet/HubBeacon.json +110 -0
  45. package/deployments/bscmainnet/HubImpl.json +2216 -0
  46. package/deployments/bscmainnet/HubRegistry.json +386 -0
  47. package/deployments/bscmainnet/HubRegistryImpl.json +386 -0
  48. package/deployments/bscmainnet/Hub_U.json +2216 -0
  49. package/deployments/bscmainnet/Hub_USDC.json +2216 -0
  50. package/deployments/bscmainnet/Hub_USDT.json +2216 -0
  51. package/deployments/bscmainnet/Migrator.json +375 -0
  52. package/deployments/bscmainnet/YieldGroupCoreImpl.json +1083 -0
  53. package/deployments/bscmainnet/YieldGroupFRVImpl.json +978 -0
  54. package/deployments/bscmainnet/YieldGroupFluxImpl.json +1083 -0
  55. package/deployments/bscmainnet_addresses.json +32 -0
  56. package/deployments/bsctestnet/AdapterCoreV1.json +336 -0
  57. package/deployments/bsctestnet/AdapterFRV.json +225 -0
  58. package/deployments/bsctestnet/AdapterFlux.json +249 -0
  59. package/deployments/bsctestnet/CoreBeacon.json +110 -0
  60. package/deployments/bsctestnet/CoreSource_USDT.json +1083 -0
  61. package/deployments/bsctestnet/FRVBeacon.json +110 -0
  62. package/deployments/bsctestnet/FRVSource_USDT.json +978 -0
  63. package/deployments/bsctestnet/FluxBeacon.json +110 -0
  64. package/deployments/bsctestnet/FluxSource_USDT.json +1083 -0
  65. package/deployments/bsctestnet/HubBeacon.json +110 -0
  66. package/deployments/bsctestnet/HubImpl.json +2216 -0
  67. package/deployments/bsctestnet/HubRegistry.json +386 -0
  68. package/deployments/bsctestnet/HubRegistryImpl.json +386 -0
  69. package/deployments/bsctestnet/HubRegistryProxyAdmin.json +157 -0
  70. package/deployments/bsctestnet/Hub_USDT.json +2216 -0
  71. package/deployments/bsctestnet/Migrator.json +375 -0
  72. package/deployments/bsctestnet/YieldGroupCoreImpl.json +1083 -0
  73. package/deployments/bsctestnet/YieldGroupFRVImpl.json +978 -0
  74. package/deployments/bsctestnet/YieldGroupFluxImpl.json +1083 -0
  75. package/deployments/bsctestnet_addresses.json +25 -0
  76. package/package.json +46 -0
@@ -0,0 +1,797 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
+ import { Address } from "@openzeppelin/contracts/utils/Address.sol";
7
+ import {
8
+ ReentrancyGuardUpgradeable
9
+ } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
10
+
11
+ import {
12
+ IAccessControlManagerV8
13
+ } from "@venusprotocol/governance-contracts/contracts/Governance/IAccessControlManagerV8.sol";
14
+
15
+ import { IHub } from "../../interfaces/IHub.sol";
16
+ import { IYieldGroupBase } from "../../interfaces/IYieldGroupBase.sol";
17
+ import { IResourceAdapter } from "../../interfaces/IResourceAdapter.sol";
18
+ import { YieldGroupBaseStorage } from "./YieldGroupBaseStorage.sol";
19
+
20
+ /**
21
+ * @title YieldGroupBase
22
+ * @author Venus
23
+ * @notice Shared base for every concrete YieldGroup — the generic `YieldGroup` (Core / Flux) and
24
+ * the `YieldGroupFRV` subclass. Holds the entire registry / queue / pause / deposit /
25
+ * withdraw / view machinery behind the standard `IYieldGroupBase` Hub boundary; deployed
26
+ * behind a proxy, one per (Hub, asset).
27
+ * @dev Architecture: Hub → YieldGroup* → IResourceAdapter (via delegatecall) → resource.
28
+ * The YieldGroup is **ABI-agnostic**: every protocol-specific concern (mint signature,
29
+ * treasury-fee gross-up, supply-cap math, APY annualisation) lives in the adapter.
30
+ * Inheritance order is load-bearing for upgrade safety — do not reorder.
31
+ *
32
+ * Family divergence is captured by four `internal virtual` hooks, all defaulting here to the
33
+ * simplest behavior; a concrete overrides only what its family needs:
34
+ * - `_prepareResource` (mutating) — advance a resource's internal state before sizing
35
+ * (FRV: `updateVaultState()`); default no-op.
36
+ * - `_minDeposit` (view) — per-resource minimum deposit floor (FRV:
37
+ * `minSupplierDeposit`); default 0.
38
+ * - `_resourceDepositRoom` (view) — spare deposit room (generic: cap-aware); default the
39
+ * raw protocol headroom.
40
+ * - `accrue` (external) — settle interest (generic: poke loop); default no-op.
41
+ *
42
+ * Stateless shared adapters: each registered resource carries an adapter address. The
43
+ * adapter is invoked via DELEGATECALL for mutating functions, so receipt tokens are
44
+ * credited to / burned from this YieldGroup directly (`msg.sender` to the underlying
45
+ * protocol is this contract, not the adapter). One adapter deployment per ABI family
46
+ * serves every YieldGroup that registers a matching resource — no per-asset adapter
47
+ * instances, no proxies, no clones.
48
+ *
49
+ * Adding a new ABI family later (e.g. Core_V2): deploy a single new stateless adapter
50
+ * and call `addResource(newResource, newAdapter)` on each affected YieldGroup. No
51
+ * changes to this contract, to the Hub, or to existing adapters.
52
+ *
53
+ * Delegatecall safety: adapter implementations MUST follow the storage-safety
54
+ * invariants documented on {IResourceAdapter} (zero state variables, no `sstore` in
55
+ * assembly, only trusted callees). `addResource` rejects EOAs to prevent silent
56
+ * no-op delegatecalls; revert bubbling is handled by `Address.functionDelegateCall`.
57
+ */
58
+ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseStorage, IYieldGroupBase {
59
+ using SafeERC20 for IERC20;
60
+ using Address for address;
61
+
62
+ // ============================== Modifiers ================================
63
+
64
+ /// @notice Reverts {NotHub} when the caller is not the registered Hub.
65
+ modifier onlyHub() {
66
+ if (msg.sender != _hub) revert NotHub(msg.sender);
67
+ _;
68
+ }
69
+
70
+ // ============================== Constructor ==============================
71
+
72
+ /// @custom:oz-upgrades-unsafe-allow constructor
73
+ constructor() {
74
+ _disableInitializers();
75
+ }
76
+
77
+ // ============================== Internal — initialisation =================
78
+
79
+ /**
80
+ * @notice Shared initialiser invoked by each concrete's public `initialize`.
81
+ * @dev Validates `IHub(hub_).asset() == asset_` so a misconfigured deployment cannot
82
+ * route the wrong underlying. There is intentionally no `blocksPerYear_ != 0` guard:
83
+ * `blocksPerYear` only annualises the informational `spotAPYBps` view, and families whose
84
+ * adapter pre-annualises (Flux) or has no per-block rate (FRV) initialise it to `0`.
85
+ * @param hub_ Hub authorised to call `deposit` / `withdraw`.
86
+ * @param asset_ Underlying ERC-20 (must match `Hub.asset()`).
87
+ * @param blocksPerYear_ Per-chain block cadence used only for spot-APY annualisation; `0` when
88
+ * the family's adapter ignores it.
89
+ * @param acm_ AccessControlManagerV8 gating governance calls.
90
+ */
91
+ function __YieldGroupBase_init(address hub_, address asset_, uint256 blocksPerYear_, address acm_)
92
+ internal
93
+ onlyInitializing
94
+ {
95
+ if (hub_ == address(0) || asset_ == address(0) || acm_ == address(0)) revert ZeroAddress();
96
+ address hubAsset = IHub(hub_).asset();
97
+ if (hubAsset != asset_) revert HubAssetMismatch(asset_, hubAsset);
98
+
99
+ __ReentrancyGuard_init();
100
+
101
+ _hub = hub_;
102
+ _assetToken = asset_;
103
+ _blocksPerYear = blocksPerYear_;
104
+ _accessControlManager = acm_;
105
+ }
106
+
107
+ // ============================== External — mutating =======================
108
+ // -- Resource registry (governance) --
109
+
110
+ /// @inheritdoc IYieldGroupBase
111
+ function addResource(address resource, address adapter) external override {
112
+ _checkAccessAllowed("addResource(address,address)");
113
+ if (resource == address(0) || adapter == address(0)) revert ZeroAddress();
114
+ if (_resourcesConfig[resource].registered) revert ResourceAlreadyRegistered(resource);
115
+ if (resource.code.length == 0) revert ResourceNotContract(resource);
116
+ // Reject EOAs explicitly: delegatecall to an EOA returns success with empty data,
117
+ // which would silently no-op every deposit/withdraw through this resource.
118
+ if (adapter.code.length == 0) revert AdapterNotContract(adapter);
119
+
120
+ // The adapter is the only contract that knows the resource's ABI; routing
121
+ // asset-match validation through it keeps this YieldGroup family-agnostic.
122
+ address resourceAsset = IResourceAdapter(adapter).asset(resource);
123
+ if (resourceAsset != _assetToken) revert ResourceAssetMismatch(resource, resourceAsset, _assetToken);
124
+
125
+ // Let the adapter veto a resource that fails a protocol-specific precondition — e.g. a
126
+ // Core market whose Comptroller charges a non-zero treasuryPercent, an exit fee that
127
+ // leaves the pool on redeem without burning shares and so is unmodeled in the gross NAV.
128
+ IResourceAdapter(adapter).validateRegistration(resource);
129
+
130
+ _resources.push(resource);
131
+ _resourceIndex[resource] = _resources.length;
132
+ _resourcesConfig[resource] = ResourceConfig({ registered: true, paused: false, adapter: adapter });
133
+
134
+ emit ResourceAdded(resource, adapter);
135
+ }
136
+
137
+ /// @inheritdoc IYieldGroupBase
138
+ function removeResource(address resource) external override {
139
+ _checkAccessAllowed("removeResource(address)");
140
+ ResourceConfig storage cfg = _resourcesConfig[resource];
141
+ if (!cfg.registered) revert ResourceNotRegistered(resource);
142
+
143
+ // Gate on the raw receipt-token balance, not the underlying value: a value-based check
144
+ // rounds a sub-unit residual to 0 and would let removal orphan dust receipt tokens.
145
+ uint256 held = IResourceAdapter(cfg.adapter).receiptBalance(resource, address(this));
146
+ if (held != 0) revert ResourceHasBalance(resource, held);
147
+
148
+ uint256 idxPlusOne = _resourceIndex[resource];
149
+ uint256 idx = idxPlusOne - 1;
150
+ uint256 lastIdx = _resources.length - 1;
151
+ if (idx != lastIdx) {
152
+ address last = _resources[lastIdx];
153
+ _resources[idx] = last;
154
+ _resourceIndex[last] = idxPlusOne;
155
+ }
156
+ _resources.pop();
157
+ delete _resourceIndex[resource];
158
+
159
+ _removeFromQueue(_innerDepositQueue, resource);
160
+ _removeFromQueue(_innerWithdrawQueue, resource);
161
+
162
+ // Clear the per-resource cap too: leaving it set would let a later re-`addResource` of the
163
+ // same address silently inherit a stale value, since `addResource` never writes it and the
164
+ // setter is registered-only. Removal restores a clean slate.
165
+ delete _resourceCap[resource];
166
+ delete _resourcesConfig[resource];
167
+
168
+ emit ResourceRemoved(resource);
169
+ }
170
+
171
+ /// @inheritdoc IYieldGroupBase
172
+ function updateResourceAdapter(address resource, address newAdapter) external override {
173
+ _checkAccessAllowed("updateResourceAdapter(address,address)");
174
+ if (newAdapter == address(0)) revert ZeroAddress();
175
+ ResourceConfig storage cfg = _resourcesConfig[resource];
176
+ if (!cfg.registered) revert ResourceNotRegistered(resource);
177
+ // Reject EOAs: delegatecall to an EOA succeeds with empty data and silently no-ops routing.
178
+ if (newAdapter.code.length == 0) revert AdapterNotContract(newAdapter);
179
+
180
+ // Validate the new adapter exactly as addResource does; the held balance and registry slot
181
+ // are untouched, only the adapter pointer changes.
182
+ address resourceAsset = IResourceAdapter(newAdapter).asset(resource);
183
+ if (resourceAsset != _assetToken) revert ResourceAssetMismatch(resource, resourceAsset, _assetToken);
184
+ IResourceAdapter(newAdapter).validateRegistration(resource);
185
+
186
+ address oldAdapter = cfg.adapter;
187
+ cfg.adapter = newAdapter;
188
+
189
+ emit ResourceAdapterUpdated(resource, oldAdapter, newAdapter);
190
+ }
191
+
192
+ // -- Inner queues (Operator) --
193
+
194
+ /// @inheritdoc IYieldGroupBase
195
+ function setInnerDepositQueue(address[] calldata queue) external override {
196
+ _checkAccessAllowed("setInnerDepositQueue(address[])");
197
+ _validateQueue(queue);
198
+ _innerDepositQueue = queue;
199
+ emit InnerDepositQueueSet(queue);
200
+ }
201
+
202
+ /// @inheritdoc IYieldGroupBase
203
+ function setInnerWithdrawQueue(address[] calldata queue) external override {
204
+ _checkAccessAllowed("setInnerWithdrawQueue(address[])");
205
+ _validateQueue(queue);
206
+ _requireWithdrawQueueCoversFundedResources(queue);
207
+ _innerWithdrawQueue = queue;
208
+ emit InnerWithdrawQueueSet(queue);
209
+ }
210
+
211
+ // -- Pause (asymmetric) --
212
+
213
+ /// @inheritdoc IYieldGroupBase
214
+ function pauseResource(address resource) external override {
215
+ _checkAccessAllowed("pauseResource(address)");
216
+ ResourceConfig storage cfg = _resourcesConfig[resource];
217
+ if (!cfg.registered) revert ResourceNotRegistered(resource);
218
+ // Idempotent: pausing an already-paused resource is a silent no-op (mirrors Hub.pauseYieldGroup).
219
+ if (!cfg.paused) {
220
+ cfg.paused = true;
221
+ emit ResourcePauseToggled(resource, true);
222
+ }
223
+ }
224
+
225
+ /// @inheritdoc IYieldGroupBase
226
+ function unpauseResource(address resource) external override {
227
+ _checkAccessAllowed("unpauseResource(address)");
228
+ ResourceConfig storage cfg = _resourcesConfig[resource];
229
+ if (!cfg.registered) revert ResourceNotRegistered(resource);
230
+ // Idempotent: unpausing an already-unpaused resource is a silent no-op (mirrors Hub).
231
+ if (cfg.paused) {
232
+ cfg.paused = false;
233
+ emit ResourcePauseToggled(resource, false);
234
+ }
235
+ }
236
+
237
+ // -- Sweep (governance) --
238
+
239
+ /// @inheritdoc IYieldGroupBase
240
+ /// @dev Bars `asset()` (idle, counted in `totalAssets`) and every registered resource: in all
241
+ /// current families the resource address *is* the receipt token this YieldGroup holds, so
242
+ /// sweeping one would drain the backing. A removed resource is unregistered, so any residual
243
+ /// it left behind is recoverable. Zero balance is a silent no-op.
244
+ function sweep(address token, address to) external override nonReentrant {
245
+ _checkAccessAllowed("sweep(address,address)");
246
+ if (to == address(0)) revert ZeroAddress();
247
+ if (token == _assetToken) revert SweepProtectedAsset();
248
+ if (_resourcesConfig[token].registered) revert SweepProtectedResource(token);
249
+
250
+ uint256 bal = IERC20(token).balanceOf(address(this));
251
+ if (bal > 0) {
252
+ IERC20(token).safeTransfer(to, bal);
253
+ emit Swept(token, to, bal);
254
+ }
255
+ }
256
+
257
+ // -- IYieldGroupBase: deposit / withdraw --
258
+
259
+ /// @inheritdoc IYieldGroupBase
260
+ function deposit(uint256 amount)
261
+ external
262
+ override(IYieldGroupBase)
263
+ onlyHub
264
+ nonReentrant
265
+ returns (uint256 deposited)
266
+ {
267
+ IERC20(_assetToken).safeTransferFrom(msg.sender, address(this), amount);
268
+
269
+ uint256 remaining = amount;
270
+ address[] storage queue = _innerDepositQueue;
271
+ uint256 len = queue.length;
272
+ for (uint256 i; i < len && remaining > 0; ++i) {
273
+ remaining -= _tryDepositOneResource(queue[i], remaining);
274
+ }
275
+
276
+ // Refund the unplaced remainder to the caller and return the amount actually placed.
277
+ // The Hub routes the refund on to the next YieldGroup in its queue.
278
+ if (remaining > 0) IERC20(_assetToken).safeTransfer(msg.sender, remaining);
279
+ return amount - remaining;
280
+ }
281
+
282
+ /// @inheritdoc IYieldGroupBase
283
+ function withdraw(uint256 amount, address to) external override(IYieldGroupBase) onlyHub nonReentrant {
284
+ uint256 remaining = amount;
285
+
286
+ // Use idle first (free, no redeem). Idle accumulates from gross-up surplus on past
287
+ // withdrawals; consuming it before touching a resource keeps balances clean.
288
+ uint256 idle = IERC20(_assetToken).balanceOf(address(this));
289
+ if (idle > 0) {
290
+ uint256 fromIdle = idle < remaining ? idle : remaining;
291
+ IERC20(_assetToken).safeTransfer(to, fromIdle);
292
+ remaining -= fromIdle;
293
+ }
294
+
295
+ address[] storage queue = _innerWithdrawQueue;
296
+ uint256 len = queue.length;
297
+ for (uint256 i; i < len && remaining > 0; ++i) {
298
+ remaining -= _tryWithdrawOneResource(queue[i], remaining, to);
299
+ }
300
+
301
+ if (remaining > 0) revert ResourceLiquidityInsufficient(amount, amount - remaining);
302
+ }
303
+
304
+ /// @inheritdoc IYieldGroupBase
305
+ /// @dev Base default: no-op. Block-lazy families (the generic `YieldGroup` serving Core)
306
+ /// override this to poke each registered resource and settle interest up to this block;
307
+ /// preview-NAV families (Flux, FRV) keep the no-op because their value is read live.
308
+ // solhint-disable-next-line no-empty-blocks
309
+ function accrue() external virtual override(IYieldGroupBase) onlyHub { }
310
+
311
+ // -- IYieldGroupBase: resource-targeted reallocation (Operator, via Hub) --
312
+
313
+ /// @inheritdoc IYieldGroupBase
314
+ /// @dev Bypasses the inner deposit queue to place the full `amount` into one resource.
315
+ /// Reverts on under-capacity rather than partial-filling, so the Hub's reallocate
316
+ /// net-zero accounting holds. Refuses paused resources (do not route into a paused
317
+ /// market — mirrors the queue path skipping them). The resource need not be in
318
+ /// `_innerWithdrawQueue`: value placed outside it counts in `totalAssets()` but is not
319
+ /// reached by the withdraw cascade until it is (re)added via {setInnerWithdrawQueue}.
320
+ function depositResource(address resource, uint256 amount)
321
+ external
322
+ override(IYieldGroupBase)
323
+ onlyHub
324
+ nonReentrant
325
+ returns (uint256 deposited)
326
+ {
327
+ ResourceConfig storage cfg = _resourcesConfig[resource];
328
+ if (!cfg.registered) revert ResourceNotRegistered(resource);
329
+ if (cfg.paused) revert ResourceIsPaused(resource);
330
+
331
+ IERC20(_assetToken).safeTransferFrom(msg.sender, address(this), amount);
332
+
333
+ _prepareResource(resource);
334
+
335
+ address adapter = cfg.adapter;
336
+ uint256 room = _resourceDepositRoom(resource, adapter);
337
+ if (amount > room) revert ResourceCapacityExceeded(amount, room);
338
+
339
+ // A subclass that imposes a per-resource minimum deposit (e.g. FRV's `minSupplierDeposit`)
340
+ // rejects a sub-minimum amount unless it is the exact residual tail (`amount == room`),
341
+ // which a resource always accepts. Reject it cleanly here rather than bubbling the
342
+ // resource's own opaque error. For the generic `YieldGroup`, `_minDeposit` is 0 and this
343
+ // block never triggers.
344
+ if (amount < room) {
345
+ uint256 minDeposit = _minDeposit(resource);
346
+ if (amount < minDeposit) revert ResourceBelowMinimumDeposit(resource, amount, minDeposit);
347
+ }
348
+
349
+ _dispatchDeposit(adapter, resource, amount);
350
+ return amount;
351
+ }
352
+
353
+ /// @inheritdoc IYieldGroupBase
354
+ /// @dev Bypasses both the idle-first path and the inner withdraw queue: redeems the full
355
+ /// `amount` from one resource and delivers it to `to`, or reverts. Pulling from a
356
+ /// paused resource is allowed (winding down a paused market is the intended use).
357
+ function withdrawResource(address resource, uint256 amount, address to)
358
+ external
359
+ override(IYieldGroupBase)
360
+ onlyHub
361
+ nonReentrant
362
+ {
363
+ ResourceConfig storage cfg = _resourcesConfig[resource];
364
+ if (!cfg.registered) revert ResourceNotRegistered(resource);
365
+
366
+ _prepareResource(resource);
367
+
368
+ address adapter = cfg.adapter;
369
+ uint256 liquid = IResourceAdapter(adapter).maxWithdraw(resource, address(this));
370
+ if (amount > liquid) revert ResourceLiquidityInsufficient(amount, liquid);
371
+
372
+ uint256 received = _dispatchWithdraw(adapter, resource, amount, to);
373
+ if (received != amount) revert AdapterUnderfilled(resource, amount, received);
374
+ }
375
+
376
+ // ============================== External — view ==========================
377
+
378
+ /// @inheritdoc IYieldGroupBase
379
+ function hub() external view override returns (address) {
380
+ return _hub;
381
+ }
382
+
383
+ /// @inheritdoc IYieldGroupBase
384
+ function accessControlManager() external view override returns (address) {
385
+ return _accessControlManager;
386
+ }
387
+
388
+ /// @inheritdoc IYieldGroupBase
389
+ function resources() external view override returns (address[] memory) {
390
+ return _resources;
391
+ }
392
+
393
+ /// @inheritdoc IYieldGroupBase
394
+ function resourceConfig(address resource)
395
+ external
396
+ view
397
+ override
398
+ returns (bool registered, bool paused, address adapter)
399
+ {
400
+ ResourceConfig storage cfg = _resourcesConfig[resource];
401
+ return (cfg.registered, cfg.paused, cfg.adapter);
402
+ }
403
+
404
+ /// @inheritdoc IYieldGroupBase
405
+ function innerDepositQueue() external view override returns (address[] memory) {
406
+ return _innerDepositQueue;
407
+ }
408
+
409
+ /// @inheritdoc IYieldGroupBase
410
+ function innerWithdrawQueue() external view override returns (address[] memory) {
411
+ return _innerWithdrawQueue;
412
+ }
413
+
414
+ // -- IYieldGroupBase views --
415
+
416
+ /// @inheritdoc IYieldGroupBase
417
+ function asset() external view override(IYieldGroupBase) returns (address) {
418
+ return _assetToken;
419
+ }
420
+
421
+ /// @inheritdoc IYieldGroupBase
422
+ /// @dev `Σ resource value` (via each resource's adapter) plus any idle asset on the
423
+ /// YieldGroup. Idle accrues from treasury-fee gross-up surplus on past withdrawals.
424
+ /// `virtual` so a future family can override the valuation; today every family (the generic
425
+ /// `YieldGroup` and `YieldGroupFRV`) shares this gross sum of per-resource adapter values.
426
+ function totalAssets() external view virtual override(IYieldGroupBase) returns (uint256 total) {
427
+ address[] storage list = _resources;
428
+ uint256 len = list.length;
429
+ for (uint256 i; i < len; ++i) {
430
+ address resource = list[i];
431
+ total += IResourceAdapter(_resourcesConfig[resource].adapter).totalAssets(resource, address(this));
432
+ }
433
+ total += IERC20(_assetToken).balanceOf(address(this));
434
+ }
435
+
436
+ /// @inheritdoc IYieldGroupBase
437
+ function maxDeposit() external view override(IYieldGroupBase) returns (uint256 total) {
438
+ address[] storage queue = _innerDepositQueue;
439
+ uint256 len = queue.length;
440
+ for (uint256 i; i < len; ++i) {
441
+ address resource = queue[i];
442
+ ResourceConfig storage cfg = _resourcesConfig[resource];
443
+ if (cfg.registered && !cfg.paused) {
444
+ uint256 room = _resourceDepositRoom(resource, cfg.adapter);
445
+ // Only advertise capacity the cascade can actually place for any amount <= it. A
446
+ // subclass with a per-resource minimum deposit only accepts a sub-floor amount as
447
+ // the exact residual tail; reporting room below that floor would break the
448
+ // `deposit(maxDeposit())` honesty contract, so report 0 for such a resource. For
449
+ // the generic `YieldGroup`, `_minDeposit` is 0 and `room` always passes.
450
+ uint256 minDep = _minDeposit(resource);
451
+ // solhint-disable-next-line gas-strict-inequalities
452
+ total += room >= minDep ? room : 0;
453
+ }
454
+ }
455
+ }
456
+
457
+ /// @inheritdoc IYieldGroupBase
458
+ /// @dev Includes idle (deliverable without redeem) and each unpaused queue entry's
459
+ /// net-of-protocol-fee redeemable amount.
460
+ function maxWithdraw() external view override(IYieldGroupBase) returns (uint256 total) {
461
+ address[] storage queue = _innerWithdrawQueue;
462
+ uint256 len = queue.length;
463
+ for (uint256 i; i < len; ++i) {
464
+ address resource = queue[i];
465
+ ResourceConfig storage cfg = _resourcesConfig[resource];
466
+ if (cfg.registered && !cfg.paused) {
467
+ total += IResourceAdapter(cfg.adapter).maxWithdraw(resource, address(this));
468
+ }
469
+ }
470
+ total += IERC20(_assetToken).balanceOf(address(this));
471
+ }
472
+
473
+ /// @inheritdoc IYieldGroupBase
474
+ /// @dev TVL-weighted across registered resources. Each adapter returns its own spot APY;
475
+ /// we weight by underlying value held. Saturates at `uint64.max` to defend against
476
+ /// a misreporting adapter.
477
+ function spotAPYBps() external view override(IYieldGroupBase) returns (uint64) {
478
+ uint256 totalVal;
479
+ uint256 weighted;
480
+ address[] storage list = _resources;
481
+ uint256 len = list.length;
482
+ uint256 bpy = _blocksPerYear;
483
+ for (uint256 i; i < len; ++i) {
484
+ address resource = list[i];
485
+ address adapter = _resourcesConfig[resource].adapter;
486
+ uint256 resVal = IResourceAdapter(adapter).totalAssets(resource, address(this));
487
+ if (resVal > 0) {
488
+ uint256 apy = IResourceAdapter(adapter).spotAPYBps(resource, bpy);
489
+ weighted += apy * resVal;
490
+ totalVal += resVal;
491
+ }
492
+ }
493
+ if (totalVal == 0) return 0;
494
+ uint256 spot = weighted / totalVal;
495
+ // forge-lint: disable-next-line(unsafe-typecast)
496
+ return spot > type(uint64).max ? type(uint64).max : uint64(spot);
497
+ }
498
+
499
+ // ============================== Internal — view ==========================
500
+
501
+ /**
502
+ * @notice ACM check used by every governance-gated entry point.
503
+ * @param signature ACM role string to authorise against `msg.sender`.
504
+ */
505
+ function _checkAccessAllowed(string memory signature) internal view {
506
+ if (!IAccessControlManagerV8(_accessControlManager).isAllowedToCall(msg.sender, signature)) {
507
+ revert Unauthorized(msg.sender, signature);
508
+ }
509
+ }
510
+
511
+ // ============================== Private — mutating =======================
512
+
513
+ /**
514
+ * @notice Try to deposit up to `remaining` into one resource via its adapter. Skips
515
+ * unregistered / paused.
516
+ * @dev Delegatecalls the adapter so receipt tokens (e.g. vTokens) are credited to this
517
+ * YieldGroup, not the adapter. `Address.functionDelegateCall` validates the adapter
518
+ * is a contract and bubbles any revert reason.
519
+ * @param resource Resource to deposit into.
520
+ * @param remaining Underlying units still needing placement.
521
+ * @return placed Underlying units placed into `resource`.
522
+ */
523
+ function _tryDepositOneResource(address resource, uint256 remaining) private returns (uint256 placed) {
524
+ ResourceConfig storage cfg = _resourcesConfig[resource];
525
+ if (!cfg.registered || cfg.paused) return 0;
526
+
527
+ // Advance the resource's internal state (FRV: `updateVaultState()`) BEFORE sizing, so the
528
+ // room read reflects current state. Stays OUTSIDE the dispatch try/catch below — a poke
529
+ // failure is a genuinely broken resource, not the route-around-on-revert class.
530
+ _prepareResource(resource);
531
+
532
+ address adapter = cfg.adapter;
533
+ uint256 room = _resourceDepositRoom(resource, adapter);
534
+ uint256 toDeposit = remaining < room ? remaining : room;
535
+ if (toDeposit == 0) return 0;
536
+
537
+ // A subclass with a per-resource minimum deposit skips a sub-floor leg that is not the exact
538
+ // residual tail (`toDeposit == room`), so the cascade continues to the next resource instead
539
+ // of reverting with the resource's own minimum-deposit error. For the generic `YieldGroup`,
540
+ // `_minDeposit` is 0 and this is dead.
541
+ if (toDeposit < room && toDeposit < _minDeposit(resource)) return 0;
542
+
543
+ // Route around a resource whose deposit reverts (e.g. its external protocol paused it after
544
+ // the views certified capacity) instead of bubbling. The adapter's allowance is set inside
545
+ // the delegatecall and rolls back atomically on revert, so nothing to reset here. A skip
546
+ // returns 0, so the cascade's backstop still reverts the whole op when no healthy resource
547
+ // can absorb the full amount.
548
+ try this.dispatchDepositExternal(adapter, resource, toDeposit) returns (uint256 p) {
549
+ return p;
550
+ } catch {
551
+ emit ResourceSkipped(resource, true);
552
+ return 0;
553
+ }
554
+ }
555
+
556
+ /**
557
+ * @notice Try to withdraw up to `remaining` from one resource via its adapter. Skips
558
+ * unregistered / paused.
559
+ * @dev Delegatecalls the adapter so `redeemUnderlying` (or equivalent) burns this
560
+ * YieldGroup's receipt tokens and credits underlying to `to`. The recipient's balance
561
+ * delta is measured and, in the success branch (outside the skip `catch`), asserted equal
562
+ * to `toDeliver`; a shortfall reverts {AdapterUnderfilled} and halts the whole withdraw
563
+ * rather than being silently routed around.
564
+ * @param resource Resource to pull from.
565
+ * @param remaining Underlying units still needing collection.
566
+ * @param to Recipient of the underlying.
567
+ * @return delivered Underlying units delivered to `to` from this resource.
568
+ */
569
+ function _tryWithdrawOneResource(address resource, uint256 remaining, address to)
570
+ private
571
+ returns (uint256 delivered)
572
+ {
573
+ ResourceConfig storage cfg = _resourcesConfig[resource];
574
+ if (!cfg.registered || cfg.paused) return 0;
575
+
576
+ // Advance the resource's internal state (FRV: `updateVaultState()`, which may unlock funds
577
+ // via a lifecycle transition) before reading liquidity. Outside the dispatch try/catch.
578
+ _prepareResource(resource);
579
+
580
+ address adapter = cfg.adapter;
581
+ uint256 liquid = IResourceAdapter(adapter).maxWithdraw(resource, address(this));
582
+ uint256 toDeliver = remaining < liquid ? remaining : liquid;
583
+ if (toDeliver == 0) return 0;
584
+
585
+ // Skip-on-revert for the withdraw leg (see `_tryDepositOneResource`): an external-protocol
586
+ // revert (pause, cap, reentrancy-blocked leg) lands in `catch` and routes around this
587
+ // resource. Under-delivery is OUR own accounting invariant, not a route-around case, so it
588
+ // is asserted in the success branch below; a revert there is not caught by the `catch` and
589
+ // propagates, halting the whole withdraw loudly. This mirrors the Hub's own YieldGroup
590
+ // underfill guard in `_tryWithdrawOneYieldGroup` and needs no error-selector matching.
591
+ try this.dispatchWithdrawExternal(adapter, resource, toDeliver, to) returns (uint256 received) {
592
+ if (received != toDeliver) revert AdapterUnderfilled(resource, toDeliver, received);
593
+ return received;
594
+ } catch {
595
+ emit ResourceSkipped(resource, false);
596
+ return 0;
597
+ }
598
+ }
599
+
600
+ /**
601
+ * @notice Delegatecall the adapter to deposit exactly `amount` into `resource` and emit
602
+ * {DepositRouted}.
603
+ * @dev Shared by the inner-queue deposit path and the resource-targeted reallocate path.
604
+ * Callers own the registration / pause / capacity checks and must ensure this
605
+ * YieldGroup already holds `amount` of underlying. Delegatecall credits receipt
606
+ * tokens to this YieldGroup; `Address.functionDelegateCall` bubbles adapter reverts.
607
+ * @param adapter Adapter bound to `resource`.
608
+ * @param resource Resource to deposit into.
609
+ * @param amount Underlying units to place (exact).
610
+ */
611
+ function _dispatchDeposit(address adapter, address resource, uint256 amount) private {
612
+ adapter.functionDelegateCall(abi.encodeCall(IResourceAdapter.deposit, (resource, amount)));
613
+ emit DepositRouted(resource, amount);
614
+ }
615
+
616
+ /**
617
+ * @notice Delegatecall the adapter to redeem `amount` from `resource` to `to`, emit
618
+ * {WithdrawRouted}, and return the recipient's measured balance delta.
619
+ * @dev Shared by the inner-queue withdraw path and the resource-targeted reallocate path.
620
+ * Callers own the registration / liquidity checks and the under-delivery invariant: this
621
+ * returns the amount actually received so each caller can assert `received == amount` in
622
+ * its own frame (a shortfall reverts {AdapterUnderfilled}). Deliberately does NOT revert on
623
+ * a shortfall itself — doing so inside the cascade's external self-call frame would blur
624
+ * our own invariant with an external-protocol revert and force selector-matching to tell
625
+ * them apart. The delta is cheap defensive verification against a future-adapter regression.
626
+ * @param adapter Adapter bound to `resource`.
627
+ * @param resource Resource to pull from.
628
+ * @param amount Underlying units to deliver to `to` (exact).
629
+ * @param to Recipient of the underlying.
630
+ * @return received Underlying units actually delivered to `to`, measured by balance delta.
631
+ */
632
+ function _dispatchWithdraw(address adapter, address resource, uint256 amount, address to)
633
+ private
634
+ returns (uint256 received)
635
+ {
636
+ IERC20 assetToken = IERC20(_assetToken);
637
+ uint256 preBalTo = assetToken.balanceOf(to);
638
+ adapter.functionDelegateCall(abi.encodeCall(IResourceAdapter.withdraw, (resource, amount, to)));
639
+ received = assetToken.balanceOf(to) - preBalTo;
640
+
641
+ emit WithdrawRouted(resource, amount);
642
+ }
643
+
644
+ /**
645
+ * @notice External self-call shell so the inner deposit cascade can `try/catch` a per-resource
646
+ * dispatch — a library delegatecall cannot be wrapped directly. Reverts unless the
647
+ * caller is this YieldGroup itself.
648
+ * @dev Runs the shared {_dispatchDeposit} body in external context. `address(this)` stays the
649
+ * YieldGroup, so the adapter's `onlyDelegateCall` check still passes. NOT `nonReentrant`
650
+ * (the cascade self-call must not trip the guard held by the public entrypoint) and NOT
651
+ * used by the strict reallocate path, which keeps calling `_dispatchDeposit` directly.
652
+ * @param adapter Adapter bound to `resource`.
653
+ * @param resource Resource to deposit into.
654
+ * @param amount Underlying units to place (exact).
655
+ * @return placed Underlying units placed (equals `amount` on success).
656
+ */
657
+ // solhint-disable-next-line ordering
658
+ function dispatchDepositExternal(address adapter, address resource, uint256 amount)
659
+ external
660
+ returns (uint256 placed)
661
+ {
662
+ if (msg.sender != address(this)) revert OnlySelf();
663
+ _dispatchDeposit(adapter, resource, amount);
664
+ return amount;
665
+ }
666
+
667
+ /**
668
+ * @notice External self-call shell so the inner withdraw cascade can `try/catch` a per-resource
669
+ * dispatch. Same guard and context guarantees as {dispatchDepositExternal}.
670
+ * @param adapter Adapter bound to `resource`.
671
+ * @param resource Resource to pull from.
672
+ * @param amount Underlying units to deliver to `to` (exact).
673
+ * @param to Recipient of the underlying.
674
+ * @return received Underlying units actually delivered (the caller asserts `received == amount`
675
+ * in its own frame; see {_tryWithdrawOneResource}).
676
+ */
677
+ // solhint-disable-next-line ordering
678
+ function dispatchWithdrawExternal(address adapter, address resource, uint256 amount, address to)
679
+ external
680
+ returns (uint256 received)
681
+ {
682
+ if (msg.sender != address(this)) revert OnlySelf();
683
+ return _dispatchWithdraw(adapter, resource, amount, to);
684
+ }
685
+
686
+ /**
687
+ * @notice Remove the first occurrence of `target` from `queue` via swap-and-pop.
688
+ * @param queue Storage array to mutate.
689
+ * @param target Address to remove (no-op if absent).
690
+ */
691
+ function _removeFromQueue(address[] storage queue, address target) internal {
692
+ uint256 len = queue.length;
693
+ for (uint256 i; i < len; ++i) {
694
+ if (queue[i] == target) {
695
+ if (i != len - 1) {
696
+ queue[i] = queue[len - 1];
697
+ }
698
+ queue.pop();
699
+ return;
700
+ }
701
+ }
702
+ }
703
+
704
+ // ============================== Private — view ===========================
705
+
706
+ /**
707
+ * @notice Validate a routing queue: every entry registered, no duplicates.
708
+ * @param queue Candidate queue.
709
+ */
710
+ function _validateQueue(address[] calldata queue) private view {
711
+ uint256 len = queue.length;
712
+ for (uint256 i; i < len; ++i) {
713
+ address resource = queue[i];
714
+ if (!_resourcesConfig[resource].registered) revert ResourceNotRegistered(resource);
715
+ for (uint256 j = i + 1; j < len; ++j) {
716
+ if (queue[j] == resource) revert InvalidQueue();
717
+ }
718
+ }
719
+ }
720
+
721
+ /**
722
+ * @notice Revert if `queue` omits any registered resource that still holds receipt tokens.
723
+ * @param queue Candidate withdraw queue.
724
+ */
725
+ function _requireWithdrawQueueCoversFundedResources(address[] calldata queue) private view {
726
+ address[] storage list = _resources;
727
+ uint256 len = list.length;
728
+ for (uint256 i; i < len; ++i) {
729
+ address resource = list[i];
730
+ uint256 held = IResourceAdapter(_resourcesConfig[resource].adapter).receiptBalance(resource, address(this));
731
+ if (held != 0 && !_queueContains(queue, resource)) {
732
+ revert WithdrawQueueOmitsFundedResource(resource);
733
+ }
734
+ }
735
+ }
736
+
737
+ /**
738
+ * @notice Linear membership test over a calldata address array.
739
+ * @param queue Array to scan.
740
+ * @param target Address to find.
741
+ * @return found True iff `target` appears in `queue`.
742
+ */
743
+ function _queueContains(address[] calldata queue, address target) private pure returns (bool found) {
744
+ uint256 len = queue.length;
745
+ for (uint256 i; i < len; ++i) {
746
+ if (queue[i] == target) return true;
747
+ }
748
+ return false;
749
+ }
750
+
751
+ // ============================== Internal — virtual hooks =================
752
+ // These capture the entire per-family divergence; each concrete overrides only what it needs.
753
+
754
+ /**
755
+ * @notice Advance a resource's internal state before a deposit/withdraw sizing read.
756
+ * @dev Base default: no-op. `YieldGroupFRV` overrides to call `IFRVVault.updateVaultState()`.
757
+ * Mutating, and invoked OUTSIDE the cascade try/catch so a poke failure halts the
758
+ * operation rather than being routed around.
759
+ */
760
+ // solhint-disable-next-line no-empty-blocks
761
+ function _prepareResource(
762
+ address /* resource */
763
+ )
764
+ internal
765
+ virtual { }
766
+
767
+ /**
768
+ * @notice Per-resource minimum deposit floor.
769
+ * @dev Base default: `0` (no floor). `YieldGroupFRV` overrides to return the vault's
770
+ * `minSupplierDeposit`. A non-zero floor makes a sub-floor non-tail deposit skip (cascade)
771
+ * or revert (targeted) instead of failing inside the resource.
772
+ * @return minimum Minimum deposit in asset units (`0` = no floor).
773
+ */
774
+ function _minDeposit(
775
+ address /* resource */
776
+ )
777
+ internal
778
+ view
779
+ virtual
780
+ returns (uint256 minimum)
781
+ {
782
+ return 0;
783
+ }
784
+
785
+ /**
786
+ * @notice Spare deposit room for `resource` right now.
787
+ * @dev Base default: the resource's protocol-level headroom, unclamped. The generic
788
+ * `YieldGroup` overrides this to further clamp by its optional per-resource cap; lifecycle
789
+ * families (FRV) keep the raw headroom.
790
+ * @param resource Resource to size.
791
+ * @param adapter Adapter bound to `resource`.
792
+ * @return room Protocol-level deposit headroom for `resource`.
793
+ */
794
+ function _resourceDepositRoom(address resource, address adapter) internal view virtual returns (uint256 room) {
795
+ return IResourceAdapter(adapter).maxDeposit(resource);
796
+ }
797
+ }