@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,551 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ import { IERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/interfaces/IERC4626Upgradeable.sol";
5
+
6
+ /**
7
+ * @title IHub
8
+ * @author Venus
9
+ * @notice External interface for the per-asset Hub vault. Extends ERC-4626 with a YieldGroup
10
+ * registry, dual caps, asymmetric pause, Operator reallocation, and fee accrual.
11
+ * @dev Conventions:
12
+ * - Asset amounts are raw underlying units; BPS are out of `10_000`.
13
+ * - All mutating user-facing calls are atomic-or-revert. The Hub routes the full amount
14
+ * to / from YieldGroups (deposits route out, withdrawals pull exact, reallocate is balanced),
15
+ * so idle is normally zero; the only balance that persists is a direct `asset()` donation
16
+ * — which, since the underlying cannot be swept, is counted in `totalAssets()` (so it
17
+ * accrues to LPs) and consumed first by `withdraw`.
18
+ * - Permissions are gated by `AccessControlManagerV8`: each governance-gated function
19
+ * calls `_checkAccessAllowed` with its literal function-signature string (e.g.
20
+ * `"addYieldGroup(address,uint256,uint16)"`), which is the ACM role for that function.
21
+ * Each governance-gated function below names the role string that gates it; the
22
+ * Operator-vs-governance split is set by which addresses hold each role, not by a
23
+ * hard-coded branch. `deposit` / `mint` / `withdraw` / `redeem` and `accrueFees`
24
+ * are permissionless.
25
+ */
26
+ interface IHub is IERC4626Upgradeable {
27
+ /**
28
+ * @notice Per-YieldGroup configuration.
29
+ * @param absoluteCap Hard cap on the YieldGroup's holdings, in asset units.
30
+ * @param percentageCapBps Cap as fraction of `totalAssets()` in BPS. The sentinel value
31
+ * `10_000` (100%) disables the percentage component — only `absoluteCap` binds.
32
+ * For any value `< 10_000`, effective cap = `min(absoluteCap,
33
+ * percentageCapBps × totalAssets() / 10_000)`. The `10_000` carve-out exists so
34
+ * a freshly-deployed Hub can take its first deposit (otherwise `pct × 0 / 10_000`
35
+ * collapses the cap to 0 at TA = 0 and the vault is un-bootstrappable).
36
+ * @param paused When true, routing skips this YieldGroup; existing balance still counts.
37
+ * @param registered True iff present in `registeredYieldGroups()`.
38
+ */
39
+ struct YieldGroupConfig {
40
+ uint256 absoluteCap;
41
+ uint16 percentageCapBps;
42
+ bool paused;
43
+ bool registered;
44
+ }
45
+
46
+ /**
47
+ * @notice One leg of a `reallocate()` plan. The same shape is reused for both the
48
+ * withdraw (pull) and the deposit (push) arrays — direction-specific semantics
49
+ * are documented on `reallocate()`.
50
+ * @param yieldGroup YieldGroup to act on. Must be registered. Deposit (push) legs additionally
51
+ * require the YieldGroup to be unpaused and to have spare cap.
52
+ * @param resource Specific resource (market / vault) inside `yieldGroup`. The sentinel
53
+ * `address(0)` means "no target": the YieldGroup cascades through its inner queue —
54
+ * idle-first on a pull leg, capacity-order on a push leg. A non-zero value moves
55
+ * the full `amount` against that one resource and bypasses the queue.
56
+ * @param amount Asset units to move on this leg.
57
+ */
58
+ struct ReallocateLeg {
59
+ address yieldGroup;
60
+ address resource;
61
+ uint256 amount;
62
+ }
63
+
64
+ // --------------------------------- Events --------------------------------
65
+
66
+ /// @notice Emitted when a YieldGroup is added to the registry
67
+ /// @param yieldGroup Newly-registered YieldGroup
68
+ /// @param absoluteCap Initial absolute cap in asset units
69
+ /// @param percentageCapBps Initial percentage cap in BPS
70
+ event YieldGroupAdded(address indexed yieldGroup, uint256 absoluteCap, uint16 percentageCapBps);
71
+
72
+ /// @notice Emitted when a YieldGroup is removed from the registry
73
+ /// @param yieldGroup Removed YieldGroup
74
+ event YieldGroupRemoved(address indexed yieldGroup);
75
+
76
+ /// @notice Emitted when a YieldGroup's caps are raised
77
+ /// @param yieldGroup YieldGroup whose caps were updated
78
+ /// @param absoluteCap New absolute cap in asset units
79
+ /// @param percentageCapBps New percentage cap in BPS
80
+ event YieldGroupCapRaised(address indexed yieldGroup, uint256 absoluteCap, uint16 percentageCapBps);
81
+
82
+ /// @notice Emitted when a YieldGroup's caps are lowered
83
+ /// @param yieldGroup YieldGroup whose caps were updated
84
+ /// @param absoluteCap New absolute cap in asset units
85
+ /// @param percentageCapBps New percentage cap in BPS
86
+ event YieldGroupCapLowered(address indexed yieldGroup, uint256 absoluteCap, uint16 percentageCapBps);
87
+
88
+ /// @notice Emitted when a YieldGroup's pause flag is flipped
89
+ /// @param yieldGroup YieldGroup whose flag flipped
90
+ /// @param paused New pause state
91
+ event YieldGroupPauseToggled(address indexed yieldGroup, bool paused);
92
+
93
+ /// @notice Emitted when the deposit routing order is replaced
94
+ /// @param queue Full replacement queue in deposit-priority order
95
+ event OuterDepositQueueSet(address[] queue);
96
+
97
+ /// @notice Emitted when the withdraw routing order is replaced
98
+ /// @param queue Full replacement queue in withdraw-priority order
99
+ event OuterWithdrawQueueSet(address[] queue);
100
+
101
+ /// @notice Emitted when the Hub pause flag is flipped
102
+ /// @param paused New Hub pause state
103
+ event HubPauseToggled(bool paused);
104
+
105
+ /// @notice Emitted when the per-request withdraw cap is raised
106
+ /// @param oldSize Previous cap in asset units
107
+ /// @param newSize New cap in asset units
108
+ event MaxWithdrawalSizeRaised(uint256 oldSize, uint256 newSize);
109
+
110
+ /// @notice Emitted when the per-request withdraw cap is lowered
111
+ /// @param oldSize Previous cap in asset units
112
+ /// @param newSize New cap in asset units
113
+ event MaxWithdrawalSizeLowered(uint256 oldSize, uint256 newSize);
114
+
115
+ /// @notice Emitted when assets are deposited into a YieldGroup on a deposit / mint / reallocate-push leg
116
+ /// @param yieldGroup YieldGroup that received the deposit
117
+ /// @param amount Asset units deposited
118
+ event DepositRouted(address indexed yieldGroup, uint256 amount);
119
+
120
+ /// @notice Emitted when assets are withdrawn from a YieldGroup on a withdraw / redeem / reallocate-pull leg
121
+ /// @param yieldGroup YieldGroup that was pulled from
122
+ /// @param amount Asset units withdrawn
123
+ event WithdrawRouted(address indexed yieldGroup, uint256 amount);
124
+
125
+ /// @notice Emitted when a deposit / withdraw cascade leg was skipped because the YieldGroup's mutating
126
+ /// call reverted (e.g. its external protocol paused it). The operation routes around the
127
+ /// YieldGroup instead of bubbling the revert; a healthy system emits this zero times.
128
+ /// @param yieldGroup YieldGroup whose leg was skipped
129
+ /// @param isDeposit True if the skipped leg was a deposit, false for a withdraw
130
+ event YieldGroupSkipped(address indexed yieldGroup, bool isDeposit);
131
+
132
+ /// @notice Emitted when the management fee rate is changed; pending fees accrued at OLD rate first
133
+ /// @param oldBps Previous rate in BPS
134
+ /// @param newBps New rate in BPS
135
+ event ManagementFeeBpsSet(uint16 oldBps, uint16 newBps);
136
+
137
+ /// @notice Emitted when the performance fee rate is changed; pending fees accrued at OLD rate first
138
+ /// @param oldBps Previous rate in BPS
139
+ /// @param newBps New rate in BPS
140
+ event PerformanceFeeBpsSet(uint16 oldBps, uint16 newBps);
141
+
142
+ /// @notice Emitted when the exit (withdraw/redeem) fee rate is changed
143
+ /// @param oldBps Previous rate in BPS
144
+ /// @param newBps New rate in BPS
145
+ event RedeemFeeBpsSet(uint16 oldBps, uint16 newBps);
146
+
147
+ /// @notice Emitted when the fee recipient is changed; pending fees minted to OLD recipient first
148
+ /// @param oldRecipient Previous recipient
149
+ /// @param newRecipient New recipient
150
+ event FeeRecipientSet(address oldRecipient, address newRecipient);
151
+
152
+ /// @notice Emitted when fee shares are minted in an accrual
153
+ /// @param managementShares Shares minted for the management fee
154
+ /// @param performanceShares Shares minted for the performance fee
155
+ /// @param totalShares Sum of the two
156
+ event FeesAccrued(uint256 managementShares, uint256 performanceShares, uint256 totalShares);
157
+
158
+ /// @notice Emitted when the performance HWM moves up. Units: `totalAssets × 1e18 / totalSupply`
159
+ /// @param oldHwm Previous HWM
160
+ /// @param newHwm New HWM
161
+ event HighWaterMarkUpdated(uint256 oldHwm, uint256 newHwm);
162
+
163
+ /// @notice Emitted when a stray-token balance is rescued via `sweep()`
164
+ /// @param token ERC-20 that was swept
165
+ /// @param to Recipient of the balance
166
+ /// @param amount Amount transferred
167
+ event Swept(address indexed token, address indexed to, uint256 amount);
168
+
169
+ /// @notice Emitted when a supplier records disclaimer consent alongside a deposit / mint
170
+ /// @param supplier Caller who consented (the depositor / minter)
171
+ /// @param receiver Address that received the minted shares
172
+ /// @param consentHash keccak256 hash of the disclaimer content the supplier consented to
173
+ event ConsentRecorded(address indexed supplier, address indexed receiver, bytes32 indexed consentHash);
174
+
175
+ // --------------------------------- Errors --------------------------------
176
+
177
+ /// @notice Error thrown when deposit / mint exceeds aggregate spare cap across unpaused YieldGroups
178
+ /// @param requested Asset units requested
179
+ /// @param maxAvailable Aggregate spare cap at revert time
180
+ error HubCapacityExceeded(uint256 requested, uint256 maxAvailable);
181
+
182
+ /// @notice Error thrown when withdraw / redeem exceeds aggregate liquid funds (withdraw queue plus Hub idle)
183
+ /// @param requested Asset units requested
184
+ /// @param available Aggregate liquid funds at revert time
185
+ error HubInsufficientLiquidity(uint256 requested, uint256 available);
186
+
187
+ /// @notice Error thrown when a YieldGroup returns fewer assets than requested on a deposit or withdraw leg
188
+ /// @param yieldGroup YieldGroup that under-filled
189
+ /// @param requested Asset units requested
190
+ /// @param actual Asset units delivered
191
+ error YieldGroupUnderfilled(address yieldGroup, uint256 requested, uint256 actual);
192
+
193
+ /// @notice Error thrown when withdraw / redeem exceeds the per-transaction cap
194
+ /// @param requested Asset units requested
195
+ /// @param perRequestCap Current value of `maxWithdrawalSize()`
196
+ error HubWithdrawCapExceeded(uint256 requested, uint256 perRequestCap);
197
+
198
+ /// @notice Error thrown when a user-facing mutating call is attempted while the Hub is paused
199
+ error HubPaused();
200
+
201
+ /// @notice Error thrown when a deposit or reallocate-deposit leg targets a paused YieldGroup
202
+ /// @param yieldGroup YieldGroup whose pause flag blocked the routing
203
+ error YieldGroupPaused(address yieldGroup);
204
+
205
+ /// @notice Error thrown when an operation targets a YieldGroup that is not in the registry
206
+ /// @param yieldGroup Address that failed the registry lookup
207
+ error YieldGroupNotRegistered(address yieldGroup);
208
+
209
+ /// @notice Error thrown when `addYieldGroup()` is called with an already-registered YieldGroup
210
+ /// @param yieldGroup Address that is already registered
211
+ error YieldGroupAlreadyRegistered(address yieldGroup);
212
+
213
+ /// @notice Error thrown when `removeYieldGroup()` is called while the YieldGroup still holds a non-zero balance
214
+ /// @param yieldGroup YieldGroup attempted to be removed
215
+ /// @param balance Current `yieldGroup.totalAssets()` preventing removal
216
+ error YieldGroupHasBalance(address yieldGroup, uint256 balance);
217
+
218
+ /// @notice Error thrown when a YieldGroup's `asset()` does not match the Hub's underlying
219
+ /// @param yieldGroup YieldGroup under inspection
220
+ /// @param yieldGroupAsset `asset()` reported by the YieldGroup
221
+ /// @param hubAsset Hub's own `asset()`
222
+ error YieldGroupAssetMismatch(address yieldGroup, address yieldGroupAsset, address hubAsset);
223
+
224
+ /// @notice Error thrown when a cap pair is invalid (e.g. percentage > 100%)
225
+ /// @param absoluteCap Offending absolute cap
226
+ /// @param percentageCapBps Offending percentage cap in BPS
227
+ error InvalidCap(uint256 absoluteCap, uint16 percentageCapBps);
228
+
229
+ /// @notice Error thrown when a queue contains duplicates or references an unregistered YieldGroup
230
+ error InvalidQueue();
231
+
232
+ /// @notice Error thrown when `reallocate()` withdraw and deposit sums do not match
233
+ /// @param withdrawSum Sum of withdraw-leg `amount` entries
234
+ /// @param depositSum Sum of deposit-leg `amount` entries
235
+ error ReallocateImbalanced(uint256 withdrawSum, uint256 depositSum);
236
+
237
+ /// @notice Error thrown when a `setOuterWithdrawQueue` replacement would drop a registered
238
+ /// YieldGroup that still custodies a balance, stranding its redeemable value. Drain the
239
+ /// YieldGroup via reallocate before removing it from the withdraw queue.
240
+ /// @param yieldGroup Funded YieldGroup missing from the candidate withdraw queue
241
+ error WithdrawQueueOmitsFundedYieldGroup(address yieldGroup);
242
+
243
+ /// @notice Error thrown when `sweep()` is called with `token == asset()`
244
+ error SweepProtectedAsset();
245
+
246
+ /// @notice Error thrown when a required non-zero address parameter was zero
247
+ error ZeroAddress();
248
+
249
+ /// @notice Error thrown when a required non-zero amount parameter was zero
250
+ error ZeroAmount();
251
+
252
+ /// @notice Error thrown when `decimalsOffset_` at `initialize()` exceeded `MAX_DECIMALS_OFFSET`
253
+ error InvalidDecimalsOffset();
254
+
255
+ /// @notice Error thrown when a fee rate exceeded `MAX_FEE_BPS`
256
+ /// @param bps Offending BPS rate
257
+ error InvalidFeeBps(uint16 bps);
258
+
259
+ /// @notice Error thrown when `raise*` is called with a value not strictly greater than the current one
260
+ error NotIncreasing();
261
+
262
+ /// @notice Error thrown when `lower*` is called with a value not strictly less than the current one
263
+ error NotDecreasing();
264
+
265
+ // ----------------------- YieldGroup registry (governance) --------------------
266
+
267
+ /**
268
+ * @notice Add a YieldGroup to the registry with initial caps
269
+ * @dev ACM role `"addYieldGroup(address,uint256,uint16)"` (VIP).
270
+ * @param yieldGroup YieldGroup contract address
271
+ * @param absoluteCap Initial absolute cap in asset units
272
+ * @param percentageCapBps Initial percentage cap in BPS
273
+ */
274
+ function addYieldGroup(address yieldGroup, uint256 absoluteCap, uint16 percentageCapBps) external;
275
+
276
+ /**
277
+ * @notice Remove a YieldGroup from the registry
278
+ * @dev ACM role `"removeYieldGroup(address)"` (VIP).
279
+ * @param yieldGroup YieldGroup to remove
280
+ */
281
+ function removeYieldGroup(address yieldGroup) external;
282
+
283
+ /**
284
+ * @notice Raise a YieldGroup's caps
285
+ * @dev ACM role `"raiseYieldGroupCap(address,uint256,uint16)"` (Guardian or VIP) — raising is loosening.
286
+ * @param yieldGroup YieldGroup whose caps are being raised
287
+ * @param absoluteCap New absolute cap in asset units
288
+ * @param percentageCapBps New percentage cap in BPS
289
+ */
290
+ function raiseYieldGroupCap(address yieldGroup, uint256 absoluteCap, uint16 percentageCapBps) external;
291
+
292
+ /**
293
+ * @notice Lower a YieldGroup's caps
294
+ * @dev ACM role `"lowerYieldGroupCap(address,uint256,uint16)"` (Operator or VIP) — tightening.
295
+ * @param yieldGroup YieldGroup whose caps are being lowered
296
+ * @param absoluteCap New absolute cap in asset units
297
+ * @param percentageCapBps New percentage cap in BPS
298
+ */
299
+ function lowerYieldGroupCap(address yieldGroup, uint256 absoluteCap, uint16 percentageCapBps) external;
300
+
301
+ // ----------------------- Outer queues (Operator) -------------------------
302
+
303
+ /**
304
+ * @notice Replace the deposit routing order
305
+ * @dev ACM role `"setOuterDepositQueue(address[])"` (Operator). Operator responsibility: keep
306
+ * every queue member in the outer withdraw queue. A member absent from the withdraw queue is
307
+ * funded by deposits but not reachable by the normal redeem path until it is (re)added there;
308
+ * recovery is one tx and lossless. Not enforced on-chain.
309
+ * @param queue Full replacement queue in deposit-priority order
310
+ */
311
+ function setOuterDepositQueue(address[] calldata queue) external;
312
+
313
+ /**
314
+ * @notice Replace the withdraw routing order
315
+ * @dev ACM role `"setOuterWithdrawQueue(address[])"` (Operator). Reverts
316
+ * {WithdrawQueueOmitsFundedYieldGroup} if a candidate queue drops a YieldGroup still holding a
317
+ * balance.
318
+ * @param queue Full replacement queue in withdraw-priority order
319
+ */
320
+ function setOuterWithdrawQueue(address[] calldata queue) external;
321
+
322
+ // ----------------------- Reallocate (Operator) ---------------------------
323
+
324
+ /**
325
+ * @notice Atomically rebalance assets across YieldGroups — and, optionally, across specific
326
+ * resources within a YieldGroup. Withdraw legs execute first, deposit legs second.
327
+ * `Σ withdraws == Σ deposits` is enforced.
328
+ * @dev ACM role `"reallocate((address,address,uint256)[],(address,address,uint256)[])"`
329
+ * (Operator). Each leg may name a specific `resource` to target one market/vault directly, or
330
+ * pass `address(0)` to route through the YieldGroup's inner queue (the original
331
+ * behavior). Setting both legs to the same `yieldGroup` performs an intra-YieldGroup move
332
+ * (e.g. vUSDT_v1 → vUSDT_v2). Honors per-YieldGroup caps and pause on the deposit side.
333
+ * Emits {WithdrawRouted} per pull and {DepositRouted} per push at the YieldGroup level;
334
+ * the targeted YieldGroup additionally emits its own resource-level routing event.
335
+ * @param withdraws Ordered list of (YieldGroup, resource, amount) legs to pull from.
336
+ * @param deposits Ordered list of (YieldGroup, resource, amount) legs to push into.
337
+ */
338
+ function reallocate(ReallocateLeg[] calldata withdraws, ReallocateLeg[] calldata deposits) external;
339
+
340
+ /**
341
+ * @notice Emergency variant of {reallocate} that stays callable while the Hub is paused, so
342
+ * governance can wind down / rebalance a stressed position during a freeze.
343
+ * @dev ACM role `"emergencyReallocate((address,address,uint256)[],(address,address,uint256)[])"`
344
+ * (Guardian or VIP/governance — NOT the Operator: this is the one fund-moving lever that bypasses the
345
+ * Hub pause, so it carries a separate role the Operator does not hold). Same net-zero
346
+ * (`Σ withdraws == Σ deposits`) semantics as {reallocate}; fee accrual is skipped while the
347
+ * Hub is paused so the frozen window is not charged.
348
+ * @param withdraws Ordered list of (yieldGroup, resource, amount) legs to pull from.
349
+ * @param deposits Ordered list of (yieldGroup, resource, amount) legs to push into.
350
+ */
351
+ function emergencyReallocate(ReallocateLeg[] calldata withdraws, ReallocateLeg[] calldata deposits) external;
352
+
353
+ // ----------------------- Pause (asymmetric) ------------------------------
354
+
355
+ /// @notice Pause the Hub
356
+ /// @dev ACM role `"pauseHub()"` (Operator, Guardian, or VIP).
357
+ function pauseHub() external;
358
+
359
+ /// @notice Unpause the Hub
360
+ /// @dev ACM role `"unpauseHub()"` (VIP).
361
+ function unpauseHub() external;
362
+
363
+ /**
364
+ * @notice Pause routing to a specific YieldGroup
365
+ * @dev ACM role `"pauseYieldGroup(address)"` (Operator or VIP).
366
+ * @param yieldGroup YieldGroup to pause
367
+ */
368
+ function pauseYieldGroup(address yieldGroup) external;
369
+
370
+ /**
371
+ * @notice Unpause routing to a specific YieldGroup
372
+ * @dev ACM role `"unpauseYieldGroup(address)"` (VIP).
373
+ * @param yieldGroup YieldGroup to unpause
374
+ */
375
+ function unpauseYieldGroup(address yieldGroup) external;
376
+
377
+ // ----------------------- Per-request withdraw cap ------------------------
378
+
379
+ /**
380
+ * @notice Raise the per-transaction withdraw cap
381
+ * @dev ACM role `"raiseMaxWithdrawalSize(uint256)"` (VIP) — raising is loosening.
382
+ * @param newSize New cap in asset units
383
+ */
384
+ function raiseMaxWithdrawalSize(uint256 newSize) external;
385
+
386
+ /**
387
+ * @notice Lower the per-transaction withdraw cap
388
+ * @dev ACM role `"lowerMaxWithdrawalSize(uint256)"` (Operator or VIP) — tightening.
389
+ * @param newSize New cap in asset units
390
+ */
391
+ function lowerMaxWithdrawalSize(uint256 newSize) external;
392
+
393
+ // --------------------------------- Fees ----------------------------------
394
+
395
+ /**
396
+ * @notice Set the management fee rate
397
+ * @dev ACM role `"setManagementFeeBps(uint16)"` (VIP). Pending fees accrue at the OLD rate
398
+ * first. Capped at `MAX_FEE_BPS`.
399
+ * @param bps New management fee rate in BPS
400
+ */
401
+ function setManagementFeeBps(uint16 bps) external;
402
+
403
+ /**
404
+ * @notice Set the performance fee rate
405
+ * @dev ACM role `"setPerformanceFeeBps(uint16)"` (VIP). Pending fees accrue at the OLD rate
406
+ * first. Capped at `MAX_FEE_BPS`.
407
+ * @param bps New performance fee rate in BPS
408
+ */
409
+ function setPerformanceFeeBps(uint16 bps) external;
410
+
411
+ /**
412
+ * @notice Set the exit (withdraw/redeem) fee rate, retained in the vault and socialised to
413
+ * remaining LPs. Anti-sandwich governance lever; default 0.
414
+ * @param bps New exit fee rate in BPS (≤ 5%)
415
+ */
416
+ function setRedeemFeeBps(uint16 bps) external;
417
+
418
+ /**
419
+ * @notice Current exit (withdraw/redeem) fee rate in BPS.
420
+ * @return bps Exit fee rate in BPS (0 by default).
421
+ */
422
+ function redeemFeeBps() external view returns (uint16 bps);
423
+
424
+ /**
425
+ * @notice Set the fee-share recipient
426
+ * @dev ACM role `"setFeeRecipient(address)"` (VIP). Pending fees mint to the OLD recipient
427
+ * first; `address(0)` disables fee minting.
428
+ * @param recipient New fee-share recipient
429
+ */
430
+ function setFeeRecipient(address recipient) external;
431
+
432
+ /// @notice Public poke that brings fee state up to date
433
+ /// @dev Permissionless.
434
+ function accrueFees() external;
435
+
436
+ // --------------------------------- Sweep ----------------------------------
437
+
438
+ /**
439
+ * @notice Forward the Hub's full balance of an arbitrary ERC-20 to a recipient
440
+ * @dev ACM role `"sweep(address,address)"` (VIP). Reverts {SweepProtectedAsset} if
441
+ * `token == asset()` — the underlying can never be swept. Callable while paused.
442
+ * @param token ERC-20 to sweep
443
+ * @param to Recipient of the balance
444
+ */
445
+ function sweep(address token, address to) external;
446
+
447
+ // ----------------------------- Supplier functions ------------------------
448
+
449
+ /**
450
+ * @notice Deposit assets and optionally record the supplier's disclaimer consent on-chain in the
451
+ * same transaction. Thin wrapper over {deposit}.
452
+ * @dev The consent hash is optional: passing `bytes32(0)` skips the emit and behaves like a plain
453
+ * {deposit}, so the frontend can use a single entry point whether or not a disclaimer applies.
454
+ * A non-zero hash emits {ConsentRecorded} before delegating, so it is rolled back with the
455
+ * transaction if the delegated {deposit} reverts. All cap / pause / accrual gating is enforced
456
+ * by {deposit}; this wrapper adds no other behaviour.
457
+ * @param assets Amount of underlying to deposit
458
+ * @param receiver Address to receive the minted shares
459
+ * @param consentHash keccak256 hash of the disclaimer content the supplier consented to;
460
+ * `bytes32(0)` skips recording (no event emitted)
461
+ * @return shares Shares minted to `receiver`
462
+ * @custom:event ConsentRecorded Emitted with the supplier, receiver, and hash when consentHash is non-zero
463
+ * @custom:error Throws HubPaused if the Hub is paused
464
+ * @custom:error Throws HubCapacityExceeded if aggregate spare cap across unpaused YieldGroups is insufficient
465
+ */
466
+ function depositWithConsent(uint256 assets, address receiver, bytes32 consentHash) external returns (uint256 shares);
467
+
468
+ /**
469
+ * @notice Mint shares and optionally record the supplier's disclaimer consent on-chain in the
470
+ * same transaction. Thin wrapper over {mint}.
471
+ * @dev The consent hash is optional: passing `bytes32(0)` skips the emit and behaves like a plain
472
+ * {mint}, so the frontend can use a single entry point whether or not a disclaimer applies. A
473
+ * non-zero hash emits {ConsentRecorded} before delegating, so it is rolled back with the
474
+ * transaction if the delegated {mint} reverts. All cap / pause / accrual gating is enforced by
475
+ * {mint}; this wrapper adds no other behaviour.
476
+ * @param shares Amount of shares to mint
477
+ * @param receiver Address to receive the minted shares
478
+ * @param consentHash keccak256 hash of the disclaimer content the supplier consented to;
479
+ * `bytes32(0)` skips recording (no event emitted)
480
+ * @return assets Underlying pulled from the caller
481
+ * @custom:event ConsentRecorded Emitted with the supplier, receiver, and hash when consentHash is non-zero
482
+ * @custom:error Throws HubPaused if the Hub is paused
483
+ * @custom:error Throws HubCapacityExceeded if aggregate spare cap is insufficient for the shares requested
484
+ */
485
+ function mintWithConsent(uint256 shares, address receiver, bytes32 consentHash) external returns (uint256 assets);
486
+
487
+ // --------------------------------- Views ---------------------------------
488
+
489
+ /**
490
+ * @notice Full config struct for a YieldGroup
491
+ * @param yieldGroup Address to look up
492
+ * @return config Stored config
493
+ */
494
+ function yieldGroupConfig(address yieldGroup) external view returns (YieldGroupConfig memory config);
495
+
496
+ /**
497
+ * @notice Live effective cap for a YieldGroup
498
+ * @param yieldGroup Address to look up
499
+ * @return cap Effective cap in asset units
500
+ */
501
+ function yieldGroupEffectiveCap(address yieldGroup) external view returns (uint256 cap);
502
+
503
+ /**
504
+ * @notice Registered YieldGroups
505
+ * @return yieldGroups Storage copy of the registry array
506
+ */
507
+ function registeredYieldGroups() external view returns (address[] memory yieldGroups);
508
+
509
+ /**
510
+ * @notice Current deposit routing order
511
+ * @return queue Storage copy of the deposit queue
512
+ */
513
+ function outerDepositQueue() external view returns (address[] memory queue);
514
+
515
+ /**
516
+ * @notice Current withdraw routing order
517
+ * @return queue Storage copy of the withdraw queue
518
+ */
519
+ function outerWithdrawQueue() external view returns (address[] memory queue);
520
+
521
+ /**
522
+ * @notice Per-transaction withdraw cap in asset units
523
+ * @return size Cap value
524
+ */
525
+ function maxWithdrawalSize() external view returns (uint256 size);
526
+
527
+ /**
528
+ * @notice Hub-level pause state
529
+ * @return paused True if `pauseHub()` is in effect
530
+ */
531
+ function hubPaused() external view returns (bool paused);
532
+
533
+ /**
534
+ * @notice Recipient of newly-minted fee shares
535
+ * @return recipient Fee recipient
536
+ */
537
+ function feeRecipient() external view returns (address recipient);
538
+
539
+ /**
540
+ * @notice Current fee rates in BPS
541
+ * @return managementBps Management fee rate
542
+ * @return performanceBps Performance fee rate
543
+ */
544
+ function feeBps() external view returns (uint16 managementBps, uint16 performanceBps);
545
+
546
+ /**
547
+ * @notice Performance HWM in share-price units
548
+ * @return hwm HWM value
549
+ */
550
+ function highWaterMarkPerShare() external view returns (uint256 hwm);
551
+ }
@@ -0,0 +1,120 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ /**
5
+ * @title IHubRegistry
6
+ * @author Venus
7
+ * @notice External interface for the chain-level Hub directory. The registry is the single
8
+ * on-chain discovery root for the Liquidity Hub topology: off-chain indexers watch
9
+ * {HubAdded} / {HubRemoved} here, then follow each Hub's own `YieldGroupAdded` /
10
+ * `YieldGroupRemoved` events to reconstruct the full hub → YieldGroup graph. One
11
+ * registry per chain; exactly one active Hub per asset.
12
+ * @dev Conventions:
13
+ * - Hubs are deployed outside the registry (the `deploy/06_DeployAssetVault` bootstrap
14
+ * script) and registered via `addHub`, which derives the asset key from
15
+ * `IHub(hub).asset()`.
16
+ * - Indexer correctness contract (NORMATIVE): on every {HubAdded}, an indexer MUST seed
17
+ * that Hub's YieldGroup set from state (`registeredYieldGroups()` + per-group config
18
+ * views). Event ordering — the onboarding VIP places `addHub` before the Hub's
19
+ * `addYieldGroup` calls in one atomic transaction, so {HubAdded} lands at a lower log
20
+ * index — is a latency optimization only, NOT a correctness requirement: it is a VIP
21
+ * convention that nothing on-chain enforces, and Hubs configured before registration
22
+ * (e.g. pre-registry deployments) emit `YieldGroupAdded` before {HubAdded} regardless.
23
+ * - The registry holds no funds and never routes assets.
24
+ * - Permissions are gated by `AccessControlManagerV8`: each governance-gated function
25
+ * calls `_checkAccessAllowed` with its literal function-signature string (e.g.
26
+ * `"addHub(address)"`), which is the ACM role for that function.
27
+ * - Replacing an asset's hub is an explicit `removeHub` followed by `addHub` — two
28
+ * events, so indexers never observe an implicit swap.
29
+ */
30
+ interface IHubRegistry {
31
+ // --------------------------------- Events --------------------------------
32
+
33
+ /// @notice Emitted when a Hub is registered for its underlying asset
34
+ /// @param asset Underlying asset of the Hub (`IHub(hub).asset()`)
35
+ /// @param hub Newly-registered Hub
36
+ event HubAdded(address indexed asset, address indexed hub);
37
+
38
+ /// @notice Emitted when a Hub is removed from the registry
39
+ /// @param asset Underlying asset the Hub was registered under
40
+ /// @param hub Removed Hub
41
+ event HubRemoved(address indexed asset, address indexed hub);
42
+
43
+ // --------------------------------- Errors --------------------------------
44
+
45
+ /// @notice Error thrown when a required non-zero address parameter was zero
46
+ error ZeroAddress();
47
+
48
+ /// @notice Error thrown when `addHub()` is called with an already-registered Hub
49
+ /// @param hub Address that is already registered
50
+ error HubAlreadyRegistered(address hub);
51
+
52
+ /// @notice Error thrown when `addHub()` would register a second Hub for an asset that
53
+ /// already has one. Replace explicitly: `removeHub(existingHub)` then `addHub(hub)`.
54
+ /// @param asset Asset that is already served
55
+ /// @param existingHub Hub currently registered for `asset`
56
+ error AssetAlreadyHasHub(address asset, address existingHub);
57
+
58
+ /// @notice Error thrown when `removeHub()` is called with a Hub that is not in the registry
59
+ /// @param hub Address that failed the registry lookup
60
+ error HubNotRegistered(address hub);
61
+
62
+ // ----------------------------- Mutating (governance) ---------------------
63
+
64
+ /**
65
+ * @notice Register an externally-deployed Hub under its underlying asset
66
+ * @dev ACM role `"addHub(address)"` (VIP). Reads `IHub(hub).asset()` to derive the key.
67
+ * In an onboarding VIP, order this call BEFORE the Hub's `addYieldGroup` calls so
68
+ * {HubAdded} precedes every `YieldGroupAdded` in the same transaction (a latency
69
+ * optimization — indexers MUST state-seed on {HubAdded} regardless; see the
70
+ * interface-level indexer contract).
71
+ * @param hub Hub contract address
72
+ */
73
+ function addHub(address hub) external;
74
+
75
+ /**
76
+ * @notice Remove a Hub from the registry
77
+ * @dev ACM role `"removeHub(address)"` (VIP).
78
+ * @param hub Hub to remove
79
+ */
80
+ function removeHub(address hub) external;
81
+
82
+ // --------------------------------- Views ---------------------------------
83
+
84
+ /**
85
+ * @notice Active Hub for an asset
86
+ * @param asset Underlying asset to look up
87
+ * @return hub Registered Hub, or `address(0)` if the asset has none
88
+ */
89
+ function hubForAsset(address asset) external view returns (address hub);
90
+
91
+ /**
92
+ * @notice The asset a Hub is registered under (reverse of {hubForAsset})
93
+ * @dev Returns the registry's asset-of-record, `address(0)` if `hub` is not registered — so it
94
+ * doubles as a registration check without a STATICCALL into a possibly-bricked Hub, and is
95
+ * authoritative even after a Hub is removed (unlike `IHub(hub).asset()`, which still reports
96
+ * the underlying).
97
+ * @param hub Hub to look up
98
+ * @return asset Underlying asset the hub is registered under, or `address(0)` if not registered
99
+ */
100
+ function assetForHub(address hub) external view returns (address asset);
101
+
102
+ /**
103
+ * @notice All registered Hubs
104
+ * @return hubs Storage copy of the registry array
105
+ */
106
+ function getHubs() external view returns (address[] memory hubs);
107
+
108
+ /**
109
+ * @notice Number of registered Hubs
110
+ * @return count Length of the registry array
111
+ */
112
+ function getHubsCount() external view returns (uint256 count);
113
+
114
+ /**
115
+ * @notice Whether an address is a registered Hub
116
+ * @param hub Address to check
117
+ * @return registered True iff `hub` is present in `getHubs()`
118
+ */
119
+ function isHub(address hub) external view returns (bool registered);
120
+ }