@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,106 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ import { IHub } from "../interfaces/IHub.sol";
5
+
6
+ /**
7
+ * @title HubStorage
8
+ * @notice Hub state, isolated in a base contract so future upgrades can extend without
9
+ * disturbing the inheritance linearisation. Add new fields after `__gap` is shrunk
10
+ * by the same number of slots; never reorder, never prepend.
11
+ *
12
+ * Slot layout (Hub's own contribution; upstream contracts have their own slots
13
+ * before this):
14
+ *
15
+ * Slot Used (bytes) Field
16
+ * ---- ------------ -----
17
+ * 0 1+1 _hubPaused, _decimalsOffsetStored
18
+ * 1 32 _maxWithdrawalSize
19
+ * 2 32 _registeredYieldGroups
20
+ * 3 32 _yieldGroupIndex
21
+ * 4 32 _yieldGroups
22
+ * 5 32 _outerDepositQueue
23
+ * 6 32 _outerWithdrawQueue
24
+ * 7 2+2+20+2 _managementFeeBps, _performanceFeeBps, _feeRecipient, _redeemFeeBps
25
+ * 8 32 _highWaterMarkPerShare
26
+ * 9 8+8+8 _lastFeeAccrualTimestamp, _pauseStart, _lastFeeAccrualBlock
27
+ * 10..49 32 × 40 __gap
28
+ */
29
+ abstract contract HubStorage {
30
+ // Slot 0: lifecycle + decimals offset.
31
+
32
+ /// @notice True while the Hub is paused.
33
+ bool internal _hubPaused;
34
+
35
+ /// @notice ERC-4626 inflation-defense offset; returned by `_decimalsOffset()`. Set once at init.
36
+ uint8 internal _decimalsOffsetStored;
37
+
38
+ // Slot 1: per-tx withdraw cap.
39
+
40
+ /// @notice Max asset units any single withdraw / redeem can move out of the Hub.
41
+ uint256 internal _maxWithdrawalSize;
42
+
43
+ // Slot 2-4: YieldGroup registry.
44
+
45
+ /// @notice Canonical set of registered YieldGroups.
46
+ address[] internal _registeredYieldGroups;
47
+
48
+ /// @notice 1-indexed position in `_registeredYieldGroups` (0 = not registered). Enables O(1) removal.
49
+ mapping(address yieldGroup => uint256 indexPlusOne) internal _yieldGroupIndex;
50
+
51
+ /// @notice Per-YieldGroup config (caps, pause, registered flag).
52
+ mapping(address yieldGroup => IHub.YieldGroupConfig config) internal _yieldGroups;
53
+
54
+ // Slot 5-6: routing queues.
55
+
56
+ /// @notice Deposit cascading order. Set by Operator.
57
+ address[] internal _outerDepositQueue;
58
+
59
+ /// @notice Withdraw pulling order. Independent of the deposit queue.
60
+ address[] internal _outerWithdrawQueue;
61
+
62
+ // Slot 7: fee parameters (packed).
63
+
64
+ /// @notice Management fee rate in BPS.
65
+ uint16 internal _managementFeeBps;
66
+
67
+ /// @notice Performance fee rate in BPS.
68
+ uint16 internal _performanceFeeBps;
69
+
70
+ /// @notice Recipient of newly-minted fee shares.
71
+ address internal _feeRecipient;
72
+
73
+ /// @notice Exit (withdraw/redeem) fee in BPS, RETAINED in the vault (socialised to remaining
74
+ /// LPs as a price-per-share uplift, not minted to a recipient). Default 0; a governance
75
+ /// anti sandwich lever for discrete settlement YieldGroups - friction against racing out
76
+ /// ahead of a downward settlement reprice (e.g. an FRV settling below its accrued mark on a
77
+ /// shortfall). Packed into the existing fee slot, so it consumes no additional storage slot.
78
+ uint16 internal _redeemFeeBps;
79
+
80
+ // Slot 8: performance HWM.
81
+
82
+ /// @notice High-water mark in share-price units (`totalAssets × 1e18 / totalSupply`).
83
+ uint256 internal _highWaterMarkPerShare;
84
+
85
+ // Slot 9: fee-accrual cursors.
86
+
87
+ /// @notice Block timestamp of last fee accrual. Drives management-fee proration only (elapsed
88
+ /// wall-clock time); it is not the per-block idempotence key (see `_lastFeeAccrualBlock`).
89
+ uint64 internal _lastFeeAccrualTimestamp;
90
+
91
+ /// @notice Block timestamp when the Hub was paused. `unpauseHub` advances
92
+ /// `_lastFeeAccrualTimestamp` by the pause duration so the frozen window is excluded
93
+ /// from management-fee accrual. Packed into slot 9 alongside `_lastFeeAccrualTimestamp`,
94
+ /// so it consumes no additional storage slot.
95
+ uint64 internal _pauseStart;
96
+
97
+ /// @notice Block number of last fee accrual, the once-per-block idempotence key for `_accrueFees`.
98
+ /// Resource NAV (a Core vToken's `exchangeRateStored`) is block-lazy: it can only change
99
+ /// when `block.number` advances. Keying idempotence off the block, not the timestamp,
100
+ /// means a fresh block always refreshes resources even when two consecutive blocks share a
101
+ /// timestamp (routine on sub-second chains). Packed into slot 9, so it adds no slot.
102
+ uint64 internal _lastFeeAccrualBlock;
103
+
104
+ /// @dev Reserved slots for upgrades. Shrink by the number of slots a new field consumes.
105
+ uint256[40] private __gap;
106
+ }
@@ -0,0 +1,292 @@
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
+
6
+ import { IHub } from "../../interfaces/IHub.sol";
7
+ import { IYieldGroupBase } from "../../interfaces/IYieldGroupBase.sol";
8
+
9
+ /**
10
+ * @title HubAdminLib
11
+ * @author Venus
12
+ * @notice Governance/operator-only ("cold") registry and queue logic for the {Hub}, extracted
13
+ * into a delegatecall-linked library to keep the Hub implementation under the EIP-170
14
+ * 24,576-byte limit.
15
+ * @dev Every function is invoked via DELEGATECALL from the Hub, so `address(this)`, the storage
16
+ * slots reached through the passed `storage` references, and the event emitter all remain
17
+ * the Hub. The logic here is a near-verbatim relocation of the former in-Hub bodies, and
18
+ * emitted events are unchanged. Only `removeYieldGroup` differs: its gate now nets the
19
+ * YieldGroup's idle asset balance out of `totalAssets()`. The library holds NO state, so
20
+ * the Hub's storage layout (see {HubStorage}) and upgrade-safety are unaffected. Access
21
+ * control (`_checkAccessAllowed`) stays in the Hub's thin wrappers, never here.
22
+ */
23
+ library HubAdminLib {
24
+ /// @notice BPS denominator (`10_000` = 100%); mirrors `Hub.BPS_DENOMINATOR`.
25
+ uint16 internal constant BPS_DENOMINATOR = 10_000;
26
+
27
+ // ============================== YieldGroup registry ===========================
28
+
29
+ /**
30
+ * @notice Add a YieldGroup to the registry with initial caps. Verbatim relocation of the former
31
+ * `Hub.addYieldGroup` body.
32
+ * @param yieldGroups Hub's per-YieldGroup config mapping (storage ref).
33
+ * @param registeredYieldGroups Hub's registry array (storage ref).
34
+ * @param yieldGroupIndex Hub's 1-indexed position mapping (storage ref).
35
+ * @param yieldGroup YieldGroup contract address (must implement IYieldGroupBase).
36
+ * @param absoluteCap Initial absolute cap in asset units.
37
+ * @param percentageCapBps Initial percentage cap in BPS (≤ 10_000).
38
+ * @param hubAsset The Hub's own `asset()`, supplied by the wrapper for the match check.
39
+ */
40
+ function addYieldGroup(
41
+ mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
42
+ address[] storage registeredYieldGroups,
43
+ mapping(address => uint256) storage yieldGroupIndex,
44
+ address yieldGroup,
45
+ uint256 absoluteCap,
46
+ uint16 percentageCapBps,
47
+ address hubAsset
48
+ ) external {
49
+ if (yieldGroup == address(0)) revert IHub.ZeroAddress();
50
+ if (yieldGroups[yieldGroup].registered) revert IHub.YieldGroupAlreadyRegistered(yieldGroup);
51
+ if (percentageCapBps > BPS_DENOMINATOR || absoluteCap == type(uint256).max) {
52
+ revert IHub.InvalidCap(absoluteCap, percentageCapBps);
53
+ }
54
+
55
+ address yieldGroupAsset = IYieldGroupBase(yieldGroup).asset();
56
+ if (yieldGroupAsset != hubAsset) revert IHub.YieldGroupAssetMismatch(yieldGroup, yieldGroupAsset, hubAsset);
57
+
58
+ registeredYieldGroups.push(yieldGroup);
59
+ yieldGroupIndex[yieldGroup] = registeredYieldGroups.length;
60
+ yieldGroups[yieldGroup] = IHub.YieldGroupConfig({
61
+ absoluteCap: absoluteCap, percentageCapBps: percentageCapBps, paused: false, registered: true
62
+ });
63
+
64
+ emit IHub.YieldGroupAdded(yieldGroup, absoluteCap, percentageCapBps);
65
+ }
66
+
67
+ /**
68
+ * @notice Remove a YieldGroup from the registry: swap-and-pop the registry array and
69
+ * cascade-strip the YieldGroup from both outer queues.
70
+ * @param yieldGroups Hub's per-YieldGroup config mapping (storage ref).
71
+ * @param registeredYieldGroups Hub's registry array (storage ref).
72
+ * @param yieldGroupIndex Hub's 1-indexed position mapping (storage ref).
73
+ * @param outerDepositQueue Hub's deposit queue (storage ref).
74
+ * @param outerWithdrawQueue Hub's withdraw queue (storage ref).
75
+ * @param yieldGroup YieldGroup to remove.
76
+ * @param hubAsset The Hub's own `asset()`, supplied by the wrapper to net idle balance out of the gate.
77
+ */
78
+ function removeYieldGroup(
79
+ mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
80
+ address[] storage registeredYieldGroups,
81
+ mapping(address => uint256) storage yieldGroupIndex,
82
+ address[] storage outerDepositQueue,
83
+ address[] storage outerWithdrawQueue,
84
+ address yieldGroup,
85
+ address hubAsset
86
+ ) external {
87
+ IHub.YieldGroupConfig storage cfg = yieldGroups[yieldGroup];
88
+ if (!cfg.registered) revert IHub.YieldGroupNotRegistered(yieldGroup);
89
+
90
+ // Normal path: refuse removal while the YieldGroup still custodies resource-backed value.
91
+ // `totalAssets()` reports that value plus any idle asset-token balance sitting directly on the
92
+ // YieldGroup. The idle term is netted out here: anyone can push dust straight to a drained
93
+ // YieldGroup, and counting it would let a 1-wei transfer block deregistration indefinitely.
94
+ // Residual idle is handled out-of-band.
95
+ //
96
+ // Emergency path: if the YieldGroup's own `totalAssets()` reverts (a bricked / buggy
97
+ // YieldGroup), removal is permitted anyway so a misbehaving YieldGroup can be evicted without
98
+ // a Hub upgrade, again with any residual value handled out-of-band.
99
+ bool balanceReadable;
100
+ uint256 yieldGroupBal;
101
+ try IYieldGroupBase(yieldGroup).totalAssets() returns (uint256 v) {
102
+ balanceReadable = true;
103
+ // Net out idle asset held directly by the YieldGroup. Only YieldGroupBase defines
104
+ // `totalAssets()` (resource-backed value + this idle balance), so the difference is
105
+ // exactly the resource-backed value. `v >= idle` always holds; the guard is defensive.
106
+ uint256 idle = IERC20(hubAsset).balanceOf(yieldGroup);
107
+ yieldGroupBal = v > idle ? v - idle : 0;
108
+ } catch {
109
+ balanceReadable = false;
110
+ }
111
+ if (balanceReadable && yieldGroupBal != 0) revert IHub.YieldGroupHasBalance(yieldGroup, yieldGroupBal);
112
+
113
+ uint256 idxPlusOne = yieldGroupIndex[yieldGroup];
114
+ uint256 idx = idxPlusOne - 1;
115
+ uint256 lastIdx = registeredYieldGroups.length - 1;
116
+ if (idx != lastIdx) {
117
+ address last = registeredYieldGroups[lastIdx];
118
+ registeredYieldGroups[idx] = last;
119
+ yieldGroupIndex[last] = idxPlusOne;
120
+ }
121
+ registeredYieldGroups.pop();
122
+ delete yieldGroupIndex[yieldGroup];
123
+
124
+ _removeFromQueue(outerDepositQueue, yieldGroup);
125
+ _removeFromQueue(outerWithdrawQueue, yieldGroup);
126
+
127
+ delete yieldGroups[yieldGroup];
128
+
129
+ emit IHub.YieldGroupRemoved(yieldGroup);
130
+ }
131
+
132
+ /**
133
+ * @notice Raise or lower a YieldGroup's caps. Merges the former symmetric `Hub.raiseYieldGroupCap`
134
+ * and `Hub.lowerYieldGroupCap` bodies behind a direction flag; the shrink/grow guard and
135
+ * the emitted event are selected by `raising`. Behavior is identical to the originals.
136
+ * @param yieldGroups Hub's per-YieldGroup config mapping (storage ref).
137
+ * @param yieldGroup YieldGroup whose caps are being changed.
138
+ * @param absoluteCap New absolute cap in asset units.
139
+ * @param percentageCapBps New percentage cap in BPS.
140
+ * @param raising True for the raise path (reverts NotIncreasing), false for lower (NotDecreasing).
141
+ */
142
+ function setYieldGroupCap(
143
+ mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
144
+ address yieldGroup,
145
+ uint256 absoluteCap,
146
+ uint16 percentageCapBps,
147
+ bool raising
148
+ ) external {
149
+ IHub.YieldGroupConfig storage cfg = yieldGroups[yieldGroup];
150
+ if (!cfg.registered) revert IHub.YieldGroupNotRegistered(yieldGroup);
151
+ if (percentageCapBps > BPS_DENOMINATOR || absoluteCap == type(uint256).max) {
152
+ revert IHub.InvalidCap(absoluteCap, percentageCapBps);
153
+ }
154
+ // Reject the wrong-direction move AND a full no-op: at least one dimension must strictly
155
+ // change in the requested direction, so a successful call always changes state and the
156
+ // emitted event is never spurious.
157
+ if (raising) {
158
+ if (
159
+ absoluteCap < cfg.absoluteCap || percentageCapBps < cfg.percentageCapBps
160
+ || (absoluteCap == cfg.absoluteCap && percentageCapBps == cfg.percentageCapBps)
161
+ ) {
162
+ revert IHub.NotIncreasing();
163
+ }
164
+ } else {
165
+ if (
166
+ absoluteCap > cfg.absoluteCap || percentageCapBps > cfg.percentageCapBps
167
+ || (absoluteCap == cfg.absoluteCap && percentageCapBps == cfg.percentageCapBps)
168
+ ) {
169
+ revert IHub.NotDecreasing();
170
+ }
171
+ }
172
+
173
+ cfg.absoluteCap = absoluteCap;
174
+ cfg.percentageCapBps = percentageCapBps;
175
+ if (raising) {
176
+ emit IHub.YieldGroupCapRaised(yieldGroup, absoluteCap, percentageCapBps);
177
+ } else {
178
+ emit IHub.YieldGroupCapLowered(yieldGroup, absoluteCap, percentageCapBps);
179
+ }
180
+ }
181
+
182
+ // ============================== Outer queues ==============================
183
+
184
+ /**
185
+ * @notice Validate a candidate deposit routing queue (every entry registered, no duplicates).
186
+ * The caller (Hub) performs the storage assignment + event, since a calldata array
187
+ * cannot be copied through a storage-pointer parameter here.
188
+ * @param yieldGroups Hub's per-YieldGroup config mapping (storage ref) for the registered check.
189
+ * @param queue Candidate queue in deposit-priority order.
190
+ */
191
+ function validateDepositQueue(
192
+ mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
193
+ address[] calldata queue
194
+ ) external view {
195
+ _validateQueue(yieldGroups, queue);
196
+ }
197
+
198
+ /**
199
+ * @notice Validate a candidate withdraw routing queue: valid (registered, no duplicates) AND
200
+ * covering every funded registered YieldGroup. The caller (Hub) performs the storage
201
+ * assignment + event.
202
+ * @param yieldGroups Hub's per-YieldGroup config mapping (storage ref) for the registered check.
203
+ * @param registeredYieldGroups Hub's registry array (storage ref) for the funded-coverage check.
204
+ * @param queue Candidate queue in withdraw-priority order.
205
+ */
206
+ function validateWithdrawQueue(
207
+ mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
208
+ address[] storage registeredYieldGroups,
209
+ address[] calldata queue
210
+ ) external view {
211
+ _validateQueue(yieldGroups, queue);
212
+ _requireWithdrawQueueCoversFundedYieldGroups(registeredYieldGroups, queue);
213
+ }
214
+
215
+ // ============================== Internal helpers ==========================
216
+
217
+ /**
218
+ * @notice Remove the first occurrence of `target` from `queue` via swap-and-pop.
219
+ * @param queue Storage array to mutate.
220
+ * @param target Address to remove (no-op if absent).
221
+ */
222
+ function _removeFromQueue(address[] storage queue, address target) private {
223
+ uint256 len = queue.length;
224
+ for (uint256 i; i < len; ++i) {
225
+ if (queue[i] == target) {
226
+ if (i != len - 1) {
227
+ queue[i] = queue[len - 1];
228
+ }
229
+ queue.pop();
230
+ return;
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * @notice Revert if the candidate withdraw queue omits any registered YieldGroup that still
237
+ * custodies a balance. A YieldGroup whose `totalAssets()` reverts is treated as funded.
238
+ * @param registeredYieldGroups Hub's registry array (storage ref).
239
+ * @param queue Candidate withdraw queue.
240
+ */
241
+ function _requireWithdrawQueueCoversFundedYieldGroups(
242
+ address[] storage registeredYieldGroups,
243
+ address[] calldata queue
244
+ ) private view {
245
+ uint256 rlen = registeredYieldGroups.length;
246
+ for (uint256 i; i < rlen; ++i) {
247
+ address yieldGroup = registeredYieldGroups[i];
248
+ bool funded;
249
+ try IYieldGroupBase(yieldGroup).totalAssets() returns (uint256 v) {
250
+ funded = v > 0;
251
+ } catch {
252
+ funded = true;
253
+ }
254
+ if (funded && !_queueContains(queue, yieldGroup)) {
255
+ revert IHub.WithdrawQueueOmitsFundedYieldGroup(yieldGroup);
256
+ }
257
+ }
258
+ }
259
+
260
+ /**
261
+ * @notice Validate a routing queue: every entry registered, no duplicates.
262
+ * @param yieldGroups Hub's per-YieldGroup config mapping (storage ref).
263
+ * @param queue Candidate queue.
264
+ */
265
+ function _validateQueue(mapping(address => IHub.YieldGroupConfig) storage yieldGroups, address[] calldata queue)
266
+ private
267
+ view
268
+ {
269
+ uint256 len = queue.length;
270
+ for (uint256 i; i < len; ++i) {
271
+ address yieldGroup = queue[i];
272
+ if (!yieldGroups[yieldGroup].registered) revert IHub.YieldGroupNotRegistered(yieldGroup);
273
+ for (uint256 j = i + 1; j < len; ++j) {
274
+ if (queue[j] == yieldGroup) revert IHub.InvalidQueue();
275
+ }
276
+ }
277
+ }
278
+
279
+ /**
280
+ * @notice Linear membership test over a calldata address array.
281
+ * @param queue Array to scan.
282
+ * @param target Address to find.
283
+ * @return found True iff `target` appears in `queue`.
284
+ */
285
+ function _queueContains(address[] calldata queue, address target) private pure returns (bool found) {
286
+ uint256 len = queue.length;
287
+ for (uint256 i; i < len; ++i) {
288
+ if (queue[i] == target) return true;
289
+ }
290
+ return false;
291
+ }
292
+ }
@@ -0,0 +1,128 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
5
+ import { IYieldGroup } from "../interfaces/IYieldGroup.sol";
6
+ import { IYieldGroupBase } from "../interfaces/IYieldGroupBase.sol";
7
+ import { YieldGroupBase } from "./base/YieldGroupBase.sol";
8
+
9
+ /**
10
+ * @title YieldGroup
11
+ * @author Venus
12
+ * @notice The generic concrete YieldGroup, shared by the synchronous families. "Core" (Venus
13
+ * vTokens) and "Flux" (Fluid fTokens) are the same contract behind different beacons; a
14
+ * family is a *deployment identity* — which adapter + resources + per-resource caps +
15
+ * `blocksPerYear` value get wired into the proxy — not a code difference.
16
+ * @dev Adds to {YieldGroupBase}: the optional per-resource deposit cap (the cap-aware
17
+ * `_resourceDepositRoom` override + its raise/lower governance surface), the `blocksPerYear`
18
+ * annualiser used by `spotAPYBps`, and the `accrue()` poke loop that settles block-lazy
19
+ * interest before the Hub reads NAV. Lifecycle families (FRV) use {YieldGroupFRV} instead.
20
+ * Inheritance order is load-bearing for upgrade safety — do not reorder.
21
+ */
22
+ contract YieldGroup is YieldGroupBase, IYieldGroup {
23
+ // ============================== External — mutating =======================
24
+ // -- Initialisation --
25
+
26
+ /// @inheritdoc IYieldGroup
27
+ function initialize(address hub_, address asset_, uint256 blocksPerYear_, address acm_) external initializer {
28
+ __YieldGroupBase_init(hub_, asset_, blocksPerYear_, acm_);
29
+ }
30
+
31
+ // -- Resource caps (asymmetric) --
32
+
33
+ /// @inheritdoc IYieldGroup
34
+ function raiseResourceCap(address resource, uint256 newCap) external override {
35
+ _checkAccessAllowed("raiseResourceCap(address,uint256)");
36
+ if (!_resourcesConfig[resource].registered) revert ResourceNotRegistered(resource);
37
+ // `0` = unbounded, so compare in effective terms: a raise must be strictly looser.
38
+ if (_effectiveResourceCap(newCap) <= _effectiveResourceCap(_resourceCap[resource])) {
39
+ revert NotIncreasing();
40
+ }
41
+ _resourceCap[resource] = newCap;
42
+ emit ResourceCapRaised(resource, newCap);
43
+ }
44
+
45
+ /// @inheritdoc IYieldGroup
46
+ function lowerResourceCap(address resource, uint256 newCap) external override {
47
+ _checkAccessAllowed("lowerResourceCap(address,uint256)");
48
+ if (!_resourcesConfig[resource].registered) revert ResourceNotRegistered(resource);
49
+ // A lower must set a real (non-zero) cap strictly tighter than the current one;
50
+ // `0` means unbounded, which is a raise, not a lower.
51
+ if (newCap == 0 || _effectiveResourceCap(newCap) >= _effectiveResourceCap(_resourceCap[resource])) {
52
+ revert NotDecreasing();
53
+ }
54
+ _resourceCap[resource] = newCap;
55
+ emit ResourceCapLowered(resource, newCap);
56
+ }
57
+
58
+ // -- Blocks-per-year annualiser (governance) --
59
+
60
+ /// @inheritdoc IYieldGroup
61
+ function setBlocksPerYear(uint256 newBlocksPerYear) external override {
62
+ _checkAccessAllowed("setBlocksPerYear(uint256)");
63
+ uint256 old = _blocksPerYear;
64
+ _blocksPerYear = newBlocksPerYear;
65
+ emit BlocksPerYearSet(old, newBlocksPerYear);
66
+ }
67
+
68
+ // -- IYieldGroupBase: accrue --
69
+
70
+ /// @inheritdoc IYieldGroupBase
71
+ /// @dev Pokes every registered resource (the same set {totalAssets} sums, paused included) to
72
+ /// settle interest up to this block. Best-effort: a resource whose accrual reverts is
73
+ /// skipped and left at its last-settled rate rather than bubbling and bricking the Hub's
74
+ /// fee accrual. Resources whose NAV is a live preview (Fluid) make each call a no-op.
75
+ function accrue() external override(YieldGroupBase, IYieldGroupBase) onlyHub {
76
+ address[] storage list = _resources;
77
+ uint256 len = list.length;
78
+ for (uint256 i; i < len; ++i) {
79
+ address adapter = _resourcesConfig[list[i]].adapter;
80
+ try IResourceAdapter(adapter).accrue(list[i]) { }
81
+ catch {
82
+ emit ResourceAccrualFailed(list[i], adapter);
83
+ }
84
+ }
85
+ }
86
+
87
+ // ============================== External — view ==========================
88
+
89
+ /// @inheritdoc IYieldGroup
90
+ function blocksPerYear() external view override returns (uint256) {
91
+ return _blocksPerYear;
92
+ }
93
+
94
+ /// @inheritdoc IYieldGroup
95
+ function resourceCap(address resource) external view override returns (uint256) {
96
+ return _resourceCap[resource];
97
+ }
98
+
99
+ // ============================== Internal — view ==========================
100
+
101
+ /**
102
+ * @inheritdoc YieldGroupBase
103
+ * @dev Clamps the protocol-level headroom by this YieldGroup's optional per-resource cap.
104
+ * When no cap is set (`0`), returns the protocol headroom directly and skips the balance
105
+ * lookup entirely — the cap feature is zero-cost when unused. When set, the operator
106
+ * headroom is `cap − our current balance` (floored at zero).
107
+ */
108
+ function _resourceDepositRoom(address resource, address adapter) internal view override returns (uint256 room) {
109
+ uint256 protocolRoom = IResourceAdapter(adapter).maxDeposit(resource);
110
+ uint256 cap = _resourceCap[resource];
111
+ if (cap == 0) return protocolRoom;
112
+
113
+ uint256 held = IResourceAdapter(adapter).totalAssets(resource, address(this));
114
+ uint256 operatorRoom = cap > held ? cap - held : 0;
115
+ return protocolRoom < operatorRoom ? protocolRoom : operatorRoom;
116
+ }
117
+
118
+ /**
119
+ * @notice Map the stored cap sentinel to a comparable value: `0` (unset) becomes the max
120
+ * uint, so an unbounded cap sorts above every finite cap in the raise/lower
121
+ * comparisons and never binds in the deposit-room math.
122
+ * @param storedCap Raw value from `_resourceCap`.
123
+ * @return effective `type(uint256).max` when `storedCap == 0`, else `storedCap`.
124
+ */
125
+ function _effectiveResourceCap(uint256 storedCap) private pure returns (uint256 effective) {
126
+ return storedCap == 0 ? type(uint256).max : storedCap;
127
+ }
128
+ }
@@ -0,0 +1,107 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
5
+ import { IYieldGroupBase } from "../interfaces/IYieldGroupBase.sol";
6
+ import { IYieldGroupFRV } from "../interfaces/IYieldGroupFRV.sol";
7
+ import { IFRVVault } from "../interfaces/external/IFRVVault.sol";
8
+ import { YieldGroupBase } from "./base/YieldGroupBase.sol";
9
+
10
+ /**
11
+ * @title YieldGroupFRV
12
+ * @author Venus
13
+ * @notice Per-asset YieldGroup that aggregates one or more Venus Fixed Rate Vaults behind the
14
+ * standard `IYieldGroupBase` Hub boundary; deployed behind a proxy, one per (Hub, asset).
15
+ * @dev A thin subclass of {YieldGroupBase}. FRV-specific concerns, all expressed by overriding the
16
+ * base's virtual hooks:
17
+ * - Each vault has a lifecycle state machine (`Fundraising → Lock → Matured / Closed`).
18
+ * `_prepareResource` advances it via `vault.updateVaultState()` before every
19
+ * deposit/withdraw sizing read, so capacity / liquidity reflect current state.
20
+ * - `_minDeposit` returns the vault's `minSupplierDeposit`, so a sub-floor non-tail deposit
21
+ * is skipped (cascade) or rejected cleanly (targeted) instead of failing inside the vault.
22
+ * FRV has no per-resource cap (it inherits the base's raw `_resourceDepositRoom`) and no
23
+ * `blocksPerYear` (it initialises it to `0`; `AdapterFRV` ignores the annualiser). It adds the
24
+ * `forceRemoveResource` escape hatch for orphaning a permanently un-drainable (Closed) vault.
25
+ * Inheritance order is load-bearing for upgrade safety — do not reorder.
26
+ */
27
+ contract YieldGroupFRV is YieldGroupBase, IYieldGroupFRV {
28
+ // ============================== External — mutating =======================
29
+ // -- Initialisation --
30
+
31
+ /// @inheritdoc IYieldGroupFRV
32
+ function initialize(address hub_, address asset_, address acm_) external initializer {
33
+ // FRV has no per-block rate to annualise, so `blocksPerYear` is 0.
34
+ __YieldGroupBase_init(hub_, asset_, 0, acm_);
35
+ }
36
+
37
+ // -- Resource registry (governance) --
38
+
39
+ // -- IYieldGroupBase: accrue --
40
+
41
+ /// @inheritdoc IYieldGroupBase
42
+ /// @dev Poke every registered vault's state so the Hub's NAV read sees the current state, not a
43
+ /// stale `Fundraising` (valued at principal, no coupon). Best-effort: a reverting poke is
44
+ /// skipped, not bubbled.
45
+ function accrue() external override(YieldGroupBase, IYieldGroupBase) onlyHub {
46
+ address[] storage list = _resources;
47
+ uint256 len = list.length;
48
+ for (uint256 i; i < len; ++i) {
49
+ try IFRVVault(list[i]).updateVaultState() { }
50
+ catch {
51
+ emit ResourceAccrualFailed(list[i], _resourcesConfig[list[i]].adapter);
52
+ }
53
+ }
54
+ }
55
+
56
+ /// @inheritdoc IYieldGroupFRV
57
+ function forceRemoveResource(address resource) external override {
58
+ _checkAccessAllowed("forceRemoveResource(address)");
59
+ ResourceConfig storage cfg = _resourcesConfig[resource];
60
+ if (!cfg.registered) revert ResourceNotRegistered(resource);
61
+
62
+ // A resource may only be force-orphaned once it holds no realizable value (NAV == 0), which
63
+ // for FRV is true only of a Closed vault's residual shares (`AdapterFRV.totalAssets` values
64
+ // a Closed position at 0). Note the gate is NAV, not `maxWithdraw`: a live FRV vault is
65
+ // non-withdrawable (`maxWithdraw == 0`) all through Fundraising, Lock and PendingSettlement
66
+ // while still backing real funds, so withdrawability would wrongly pass a healthy vault.
67
+ uint256 nav = IResourceAdapter(cfg.adapter).totalAssets(resource, address(this));
68
+ if (nav != 0) revert ResourceHasValue(resource, nav);
69
+
70
+ uint256 orphaned = IResourceAdapter(cfg.adapter).receiptBalance(resource, address(this));
71
+
72
+ uint256 idxPlusOne = _resourceIndex[resource];
73
+ uint256 idx = idxPlusOne - 1;
74
+ uint256 lastIdx = _resources.length - 1;
75
+ if (idx != lastIdx) {
76
+ address last = _resources[lastIdx];
77
+ _resources[idx] = last;
78
+ _resourceIndex[last] = idxPlusOne;
79
+ }
80
+ _resources.pop();
81
+ delete _resourceIndex[resource];
82
+
83
+ _removeFromQueue(_innerDepositQueue, resource);
84
+ _removeFromQueue(_innerWithdrawQueue, resource);
85
+
86
+ delete _resourcesConfig[resource];
87
+
88
+ emit ResourceForceRemoved(resource, orphaned);
89
+ }
90
+
91
+ // ============================== Internal — virtual hooks =================
92
+
93
+ /// @dev Advance the FRV vault's lifecycle state machine before each sizing read. Without this,
94
+ /// a vault past its fundraising window would report stale capacity and the deposit call
95
+ /// inside the adapter would revert instead of gracefully skipping. Invoked outside the
96
+ /// cascade try/catch — a poke failure is a genuinely broken vault, not the skip class.
97
+ function _prepareResource(address resource) internal override {
98
+ IFRVVault(resource).updateVaultState();
99
+ }
100
+
101
+ /// @dev FRV's per-resource minimum deposit floor. A vault rejects a deposit below
102
+ /// `minSupplierDeposit` unless it exactly fills the vault's remaining capacity (the
103
+ /// residual tail), which the base routing handles via this floor.
104
+ function _minDeposit(address resource) internal view override returns (uint256) {
105
+ return IFRVVault(resource).config().minSupplierDeposit;
106
+ }
107
+ }