@venusprotocol/liquidity-hub 2.1.0-dev.4 → 2.1.0-dev.5

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 (40) hide show
  1. package/README.md +111 -30
  2. package/contracts/Hub/Hub.sol +71 -63
  3. package/contracts/adapters/AdapterCentrifuge.sol +5 -0
  4. package/contracts/adapters/AdapterCoreV1.sol +6 -0
  5. package/contracts/adapters/AdapterFRV.sol +27 -7
  6. package/contracts/adapters/AdapterFlux.sol +6 -0
  7. package/contracts/adapters/AdapterSpokeV1.sol +428 -0
  8. package/contracts/interfaces/IHub.sol +6 -0
  9. package/contracts/interfaces/IHubLens.sol +71 -0
  10. package/contracts/interfaces/IResourceAdapter.sol +23 -7
  11. package/contracts/interfaces/IYieldGroupBase.sol +6 -1
  12. package/contracts/interfaces/external/ISpokeComptroller.sol +69 -0
  13. package/contracts/interfaces/external/IVTokenIsolated.sol +153 -0
  14. package/contracts/lens/HubCapacityMath.sol +610 -0
  15. package/contracts/lens/HubLens.sol +311 -0
  16. package/deployments/bscmainnet/AdapterCentrifuge.json +21 -2
  17. package/deployments/bscmainnet/AdapterCoreV1.json +21 -2
  18. package/deployments/bscmainnet/AdapterFRV.json +21 -2
  19. package/deployments/bscmainnet/AdapterFlux.json +21 -2
  20. package/deployments/bscmainnet/AdapterSpokeV1.json +339 -0
  21. package/deployments/bscmainnet/HubImpl.json +16 -3
  22. package/deployments/bscmainnet/HubLens.json +89 -0
  23. package/deployments/bscmainnet/SpokeBeacon.json +110 -0
  24. package/deployments/bscmainnet/SpokeSource_U.json +1083 -0
  25. package/deployments/bscmainnet/SpokeSource_USDC.json +1083 -0
  26. package/deployments/bscmainnet/SpokeSource_USDT.json +1083 -0
  27. package/deployments/bscmainnet/YieldGroupSpokeImpl.json +1083 -0
  28. package/deployments/bscmainnet_addresses.json +13 -6
  29. package/deployments/bsctestnet/AdapterCentrifuge.json +21 -2
  30. package/deployments/bsctestnet/AdapterCoreV1.json +21 -2
  31. package/deployments/bsctestnet/AdapterFRV.json +21 -2
  32. package/deployments/bsctestnet/AdapterFlux.json +21 -2
  33. package/deployments/bsctestnet/AdapterSpokeV1.json +339 -0
  34. package/deployments/bsctestnet/HubImpl.json +16 -3
  35. package/deployments/bsctestnet/HubLens.json +89 -0
  36. package/deployments/bsctestnet/SpokeBeacon.json +110 -0
  37. package/deployments/bsctestnet/SpokeSource_USDT.json +1083 -0
  38. package/deployments/bsctestnet/YieldGroupSpokeImpl.json +1083 -0
  39. package/deployments/bsctestnet_addresses.json +11 -6
  40. package/package.json +1 -1
@@ -0,0 +1,311 @@
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 { IHubLens } from "../interfaces/IHubLens.sol";
8
+ import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
9
+ import { IYieldGroupBase } from "../interfaces/IYieldGroupBase.sol";
10
+ import { IFRVVault } from "../interfaces/external/IFRVVault.sol";
11
+ import { HubCapacityMath } from "./HubCapacityMath.sol";
12
+
13
+ /**
14
+ * @title HubLens
15
+ * @author Venus
16
+ * @notice Off-chain read surface for the {Hub}: deposit capacity and blended yield.
17
+ * @dev Stateless and immutable — no storage, no owner, no initializer, no proxy. The Hub is a call
18
+ * parameter rather than a stored address, so a single deployment serves `Hub_U`, `Hub_USDC`,
19
+ * `Hub_USDT` and every Hub added later. Nothing on the Hub points at this contract, so a new
20
+ * version is deployed rather than upgraded.
21
+ *
22
+ * Quantity views fail open per YieldGroup, mirroring the cascade: a YieldGroup whose
23
+ * `maxDeposit()` or `spotAPYBps()` reverts contributes 0 rather than bricking the whole view.
24
+ * NAV fails closed, inherited rather than re-implemented — every function here reads
25
+ * `Hub.totalAssets()`, which deliberately does NOT isolate a YieldGroup whose `totalAssets()`
26
+ * reverts, because a capacity or yield figure computed against a wrong NAV is worse than no
27
+ * figure at all.
28
+ */
29
+ contract HubLens is IHubLens {
30
+ /// @dev Accrual margin trimmed off each YieldGroup's room: `balance / 10_000`, i.e. 0.01%. Sized
31
+ /// on the balance, not on the room, because the interest a deposit settles scales with what
32
+ /// the group holds. 0.01% is about nine hours of interest at 10% APY, against a mark that
33
+ /// goes stale only between two accruals of the venue behind it.
34
+ uint256 private constant ACCRUAL_MARGIN_DIVISOR = 10_000;
35
+
36
+ /// @dev ABI size of `IFRVVault.config()`: nine static words. Anything else is not an FRV vault.
37
+ uint256 private constant FRV_CONFIG_SIZE = 9 * 32;
38
+
39
+ /// @inheritdoc IHubLens
40
+ function maxDeposit(address hub) public view override returns (uint256 assets) {
41
+ if (IHub(hub).hubPaused()) return 0;
42
+ (address[] memory yieldGroups, HubCapacityMath.GroupInput[] memory groups, uint256 hubTotalAssets) =
43
+ _readDepositQueue(hub);
44
+ (HubCapacityMath.ResourceFloor[][] memory floors, bool floored) = _readFloors(yieldGroups);
45
+ if (floored) {
46
+ (assets,,) = _routable(hub, groups, floors, hubTotalAssets, false);
47
+ } else {
48
+ (assets,) = HubCapacityMath.trimmedCapacity(groups, hubTotalAssets, ACCRUAL_MARGIN_DIVISOR);
49
+ }
50
+ }
51
+
52
+ /// @inheritdoc IHubLens
53
+ /// @dev Trimmed a second time: the interest the deposit settles also raises the share price it
54
+ /// prices the mint at, so these shares convert to more assets than they do here.
55
+ function maxMint(address hub) external view override returns (uint256 shares) {
56
+ if (IHub(hub).hubPaused()) return 0;
57
+ (address[] memory yieldGroups, HubCapacityMath.GroupInput[] memory groups, uint256 hubTotalAssets) =
58
+ _readDepositQueue(hub);
59
+ (HubCapacityMath.ResourceFloor[][] memory floors, bool floored) = _readFloors(yieldGroups);
60
+ if (floored) {
61
+ (shares,,) = _routable(hub, groups, floors, hubTotalAssets, true);
62
+ return shares;
63
+ }
64
+
65
+ uint256 budget = HubCapacityMath.doubleTrimmedCapacity(groups, hubTotalAssets, ACCRUAL_MARGIN_DIVISOR);
66
+
67
+ // `mint` charges `previewMint`, which rounds the assets back up, but no give-back is owed:
68
+ // the floor here already sheds more than that round-up asks for, so `previewMint(shares)`
69
+ // never passes `budget`. Both directions price off the same supply and assets.
70
+ shares = IHub(hub).convertToShares(budget);
71
+ }
72
+
73
+ /// @inheritdoc IHubLens
74
+ function depositCapacityBreakdown(address hub)
75
+ external
76
+ view
77
+ override
78
+ returns (address[] memory groups, uint256[] memory room)
79
+ {
80
+ if (IHub(hub).hubPaused()) return (groups, room);
81
+ HubCapacityMath.GroupInput[] memory inputs;
82
+ uint256 hubTotalAssets;
83
+ (groups, inputs, hubTotalAssets) = _readDepositQueue(hub);
84
+
85
+ // Split at the trimmed total, not the untrimmed one: the caps shrink with the deposit, so
86
+ // rooms solved for the untrimmed figure overstate the percentage-bound groups.
87
+ (HubCapacityMath.ResourceFloor[][] memory floors, bool floored) = _readFloors(groups);
88
+ if (floored) {
89
+ (,, room) = _routable(hub, inputs, floors, hubTotalAssets, false);
90
+ } else {
91
+ (, room) = HubCapacityMath.trimmedCapacity(inputs, hubTotalAssets, ACCRUAL_MARGIN_DIVISOR);
92
+ }
93
+ }
94
+
95
+ /// @inheritdoc IHubLens
96
+ function spotAPYBps(address hub) external view override returns (uint64 apyBps) {
97
+ // Read first: this is `Σ YieldGroup.totalAssets()` plus Hub idle, and it reverts if any
98
+ // registered YieldGroup cannot be valued — the same list this loop walks, so a per-group
99
+ // guard below would never fire.
100
+ uint256 hubTotalAssets = IHub(hub).totalAssets();
101
+ if (hubTotalAssets == 0) return 0;
102
+
103
+ address[] memory yieldGroups = IHub(hub).registeredYieldGroups();
104
+ uint256 weighted;
105
+
106
+ for (uint256 i; i < yieldGroups.length; ++i) {
107
+ address yieldGroup = yieldGroups[i];
108
+
109
+ // Weight by the invested part alone. A YieldGroup averages its rate over its
110
+ // resources, while its `totalAssets()` also counts asset sitting on the YieldGroup
111
+ // itself, which earns nothing: weighting by the total would pay that idle the rate.
112
+ uint256 balance = IYieldGroupBase(yieldGroup).totalAssets();
113
+ uint256 idle = IERC20(IYieldGroupBase(yieldGroup).asset()).balanceOf(yieldGroup);
114
+ uint256 invested = balance > idle ? balance - idle : 0;
115
+ if (invested == 0) continue;
116
+
117
+ try IYieldGroupBase(yieldGroup).spotAPYBps() returns (uint64 v) {
118
+ weighted += uint256(v) * invested;
119
+ } catch {
120
+ continue;
121
+ }
122
+ }
123
+
124
+ // Denominator is the Hub's whole TVL, not the sum of the balances weighted above, so Hub
125
+ // idle and any skipped YieldGroup dilute the figure at 0% instead of vanishing from it.
126
+ uint256 blended = weighted / hubTotalAssets;
127
+ return blended > type(uint64).max ? type(uint64).max : uint64(blended);
128
+ }
129
+
130
+ // ============================== Private =================================
131
+
132
+ /**
133
+ * @dev The largest trimmed figure the Hub routes in full when a YieldGroup has floored
134
+ * resources. `YieldGroupBase.maxDeposit()` checks each floor against the resource's own
135
+ * room, but the Hub cap and the inner queue can shrink the leg afterwards, so the solve's
136
+ * figure can land under a floor and revert. Offer the solve each floored YieldGroup's
137
+ * room candidates, largest first, and keep the first figure whose dry run routes in full.
138
+ * A failed dry run steps down the first YieldGroup that skipped part of its leg.
139
+ * @return amount Assets, or shares when `forMint`.
140
+ * @return assets Assets the Hub would route: `amount`, or `previewMint(amount)`.
141
+ * @return placed Per-YieldGroup amount placed by that routing.
142
+ */
143
+ function _routable(
144
+ address hub,
145
+ HubCapacityMath.GroupInput[] memory groups,
146
+ HubCapacityMath.ResourceFloor[][] memory floors,
147
+ uint256 hubTotalAssets,
148
+ bool forMint
149
+ ) private view returns (uint256 amount, uint256 assets, uint256[] memory placed) {
150
+ uint256 n = groups.length;
151
+ HubCapacityMath.GroupInput[] memory offered = new HubCapacityMath.GroupInput[](n);
152
+ uint256[][] memory candidates = new uint256[][](n);
153
+ uint256[] memory pick = new uint256[](n);
154
+ uint256 tries = 1;
155
+
156
+ for (uint256 i; i < n; ++i) {
157
+ HubCapacityMath.GroupInput memory g = groups[i];
158
+ offered[i] = HubCapacityMath.GroupInput({
159
+ balance: g.balance,
160
+ venueRoom: g.venueRoom,
161
+ absoluteCap: g.absoluteCap,
162
+ percentageCapBps: g.percentageCapBps
163
+ });
164
+ if (floors[i].length != 0) {
165
+ candidates[i] = HubCapacityMath.roomCandidates(floors[i], g.venueRoom);
166
+ offered[i].venueRoom = candidates[i][0];
167
+ tries += candidates[i].length;
168
+ }
169
+ }
170
+
171
+ for (uint256 t; t < tries; ++t) {
172
+ if (forMint) {
173
+ uint256 budget = HubCapacityMath.doubleTrimmedCapacity(offered, hubTotalAssets, ACCRUAL_MARGIN_DIVISOR);
174
+ amount = IHub(hub).convertToShares(budget);
175
+ assets = IHub(hub).previewMint(amount);
176
+ } else {
177
+ (amount,) = HubCapacityMath.trimmedCapacity(offered, hubTotalAssets, ACCRUAL_MARGIN_DIVISOR);
178
+ assets = amount;
179
+ }
180
+
181
+ uint256 left;
182
+ bool[] memory short;
183
+ (left, placed, short) = HubCapacityMath.route(groups, floors, hubTotalAssets, assets);
184
+ if (left == 0) return (amount, assets, placed);
185
+
186
+ uint256 i;
187
+ while (i < n && !(short[i] && pick[i] + 1 < candidates[i].length)) ++i;
188
+ if (i == n) break;
189
+ offered[i].venueRoom = candidates[i][++pick[i]];
190
+ }
191
+
192
+ return (0, 0, new uint256[](n));
193
+ }
194
+
195
+ /// @dev Per YieldGroup, its unpaused inner deposit-queue resources with their room and floor,
196
+ /// or empty when none has a floor. `floored` is true when any YieldGroup has one. Room is
197
+ /// the adapter's raw `maxDeposit`, which is what `YieldGroupFRV` deposits against.
198
+ function _readFloors(address[] memory groups)
199
+ private
200
+ view
201
+ returns (HubCapacityMath.ResourceFloor[][] memory floors, bool floored)
202
+ {
203
+ floors = new HubCapacityMath.ResourceFloor[][](groups.length);
204
+ for (uint256 i; i < groups.length; ++i) {
205
+ floors[i] = _readGroupFloors(groups[i]);
206
+ if (floors[i].length != 0) floored = true;
207
+ }
208
+ }
209
+
210
+ function _readGroupFloors(address yieldGroup) private view returns (HubCapacityMath.ResourceFloor[] memory floors) {
211
+ address[] memory queue;
212
+ try IYieldGroupBase(yieldGroup).innerDepositQueue() returns (address[] memory q) {
213
+ queue = q;
214
+ } catch {
215
+ return floors;
216
+ }
217
+
218
+ HubCapacityMath.ResourceFloor[] memory buffer = new HubCapacityMath.ResourceFloor[](queue.length);
219
+ uint256 count;
220
+ bool anyFloor;
221
+ for (uint256 k; k < queue.length; ++k) {
222
+ address resource = queue[k];
223
+ address adapter;
224
+ try IYieldGroupBase(yieldGroup).resourceConfig(resource) returns (bool registered, bool paused, address a) {
225
+ if (!registered || paused) continue;
226
+ adapter = a;
227
+ } catch {
228
+ continue;
229
+ }
230
+
231
+ uint256 room;
232
+ try IResourceAdapter(adapter).maxDeposit(resource) returns (uint256 v) {
233
+ room = v;
234
+ } catch {
235
+ continue;
236
+ }
237
+
238
+ uint256 minDeposit = _minDepositOf(resource);
239
+ if (minDeposit != 0) anyFloor = true;
240
+ buffer[count++] = HubCapacityMath.ResourceFloor({ room: room, minDeposit: minDeposit });
241
+ }
242
+ if (!anyFloor) return floors;
243
+
244
+ floors = new HubCapacityMath.ResourceFloor[](count);
245
+ for (uint256 k; k < count; ++k) {
246
+ floors[k] = buffer[k];
247
+ }
248
+ }
249
+
250
+ /// @dev `minSupplierDeposit` of an FRV vault, 0 for any resource that is not one. A raw
251
+ /// staticcall, so a vToken or fToken without `config()` reads as no floor instead of
252
+ /// reverting the view.
253
+ function _minDepositOf(address resource) private view returns (uint256) {
254
+ (bool ok, bytes memory data) = resource.staticcall(abi.encodeCall(IFRVVault.config, ()));
255
+ if (!ok || data.length != FRV_CONFIG_SIZE) return 0;
256
+ return abi.decode(data, (IFRVVault.FRVVaultConfig)).minSupplierDeposit;
257
+ }
258
+
259
+ /// @dev Read the deposit queue down to what {HubCapacityMath} needs. Sourced from that queue,
260
+ /// so a registered but unqueued YieldGroup is out of scope here, unlike {spotAPYBps}.
261
+ /// Drops every YieldGroup the cascade would skip — unregistered, paused, or failing either
262
+ /// view — so what it leaves out is immovable TVL, which the math derives from
263
+ /// `hubTotalAssets`.
264
+ function _readDepositQueue(address hub)
265
+ private
266
+ view
267
+ returns (address[] memory groups, HubCapacityMath.GroupInput[] memory inputs, uint256 hubTotalAssets)
268
+ {
269
+ // Read first, so a YieldGroup that cannot be valued halts here rather than being silently
270
+ // skipped below and then reverting anyway on this same call.
271
+ hubTotalAssets = IHub(hub).totalAssets();
272
+
273
+ address[] memory queue = IHub(hub).outerDepositQueue();
274
+ uint256 len = queue.length;
275
+
276
+ address[] memory groupBuffer = new address[](len);
277
+ HubCapacityMath.GroupInput[] memory inputBuffer = new HubCapacityMath.GroupInput[](len);
278
+ uint256 count;
279
+
280
+ for (uint256 i; i < len; ++i) {
281
+ address yieldGroup = queue[i];
282
+ IHub.YieldGroupConfig memory cfg = IHub(hub).yieldGroupConfig(yieldGroup);
283
+ if (!cfg.registered || cfg.paused) continue;
284
+
285
+ uint256 balance = IYieldGroupBase(yieldGroup).totalAssets();
286
+
287
+ uint256 venueRoom;
288
+ try IYieldGroupBase(yieldGroup).maxDeposit() returns (uint256 v) {
289
+ venueRoom = v;
290
+ } catch {
291
+ continue;
292
+ }
293
+
294
+ groupBuffer[count] = yieldGroup;
295
+ inputBuffer[count] = HubCapacityMath.GroupInput({
296
+ balance: balance,
297
+ venueRoom: venueRoom,
298
+ absoluteCap: cfg.absoluteCap,
299
+ percentageCapBps: cfg.percentageCapBps
300
+ });
301
+ ++count;
302
+ }
303
+
304
+ groups = new address[](count);
305
+ inputs = new HubCapacityMath.GroupInput[](count);
306
+ for (uint256 i; i < count; ++i) {
307
+ groups[i] = groupBuffer[i];
308
+ inputs[i] = inputBuffer[i];
309
+ }
310
+ }
311
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "address": "0x680cE4422264ecDAd3590cB50FE254D4c153f427",
3
- "block": 122006956,
2
+ "address": "0xaa9D52758890997D91950E595886FF9CDEca81ae",
3
+ "block": 123325056,
4
4
  "chainId": 56,
5
5
  "contract": "AdapterCentrifuge",
6
6
  "abi": [
@@ -295,6 +295,25 @@
295
295
  "outputs": [],
296
296
  "stateMutability": "nonpayable"
297
297
  },
298
+ {
299
+ "type": "function",
300
+ "name": "resourceName",
301
+ "inputs": [
302
+ {
303
+ "name": "resource",
304
+ "type": "address",
305
+ "internalType": "address"
306
+ }
307
+ ],
308
+ "outputs": [
309
+ {
310
+ "name": "",
311
+ "type": "string",
312
+ "internalType": "string"
313
+ }
314
+ ],
315
+ "stateMutability": "view"
316
+ },
298
317
  {
299
318
  "type": "function",
300
319
  "name": "spotAPYBps",
@@ -1,6 +1,6 @@
1
1
  {
2
- "address": "0x4E514a0C7aB9d140eE204dfA0017574270D92944",
3
- "block": 112779813,
2
+ "address": "0xa061cC2F62BDa2D6bA53Fb98847aD9822DA085dd",
3
+ "block": 123325056,
4
4
  "chainId": 56,
5
5
  "contract": "AdapterCoreV1",
6
6
  "abi": [
@@ -158,6 +158,25 @@
158
158
  ],
159
159
  "stateMutability": "view"
160
160
  },
161
+ {
162
+ "type": "function",
163
+ "name": "resourceName",
164
+ "inputs": [
165
+ {
166
+ "name": "resource",
167
+ "type": "address",
168
+ "internalType": "address"
169
+ }
170
+ ],
171
+ "outputs": [
172
+ {
173
+ "name": "",
174
+ "type": "string",
175
+ "internalType": "string"
176
+ }
177
+ ],
178
+ "stateMutability": "view"
179
+ },
161
180
  {
162
181
  "type": "function",
163
182
  "name": "spotAPYBps",
@@ -1,6 +1,6 @@
1
1
  {
2
- "address": "0x1FA0365bDd603452CE96BE3c0e12Db5515a35902",
3
- "block": 112780032,
2
+ "address": "0x1824caA965Ac05ae5C9C2819e045337e2cF5657c",
3
+ "block": 123325056,
4
4
  "chainId": 56,
5
5
  "contract": "AdapterFRV",
6
6
  "abi": [
@@ -132,6 +132,25 @@
132
132
  ],
133
133
  "stateMutability": "view"
134
134
  },
135
+ {
136
+ "type": "function",
137
+ "name": "resourceName",
138
+ "inputs": [
139
+ {
140
+ "name": "resource",
141
+ "type": "address",
142
+ "internalType": "address"
143
+ }
144
+ ],
145
+ "outputs": [
146
+ {
147
+ "name": "",
148
+ "type": "string",
149
+ "internalType": "string"
150
+ }
151
+ ],
152
+ "stateMutability": "view"
153
+ },
135
154
  {
136
155
  "type": "function",
137
156
  "name": "spotAPYBps",
@@ -1,6 +1,6 @@
1
1
  {
2
- "address": "0xA81bDf813A428053E764C34Bc679b3E4d0807be3",
3
- "block": 112780252,
2
+ "address": "0x9a1a7F6aebA4a1A3e85Ae1Dba90a7A2A52803404",
3
+ "block": 123325056,
4
4
  "chainId": 56,
5
5
  "contract": "AdapterFlux",
6
6
  "abi": [
@@ -151,6 +151,25 @@
151
151
  ],
152
152
  "stateMutability": "view"
153
153
  },
154
+ {
155
+ "type": "function",
156
+ "name": "resourceName",
157
+ "inputs": [
158
+ {
159
+ "name": "resource",
160
+ "type": "address",
161
+ "internalType": "address"
162
+ }
163
+ ],
164
+ "outputs": [
165
+ {
166
+ "name": "",
167
+ "type": "string",
168
+ "internalType": "string"
169
+ }
170
+ ],
171
+ "stateMutability": "view"
172
+ },
154
173
  {
155
174
  "type": "function",
156
175
  "name": "spotAPYBps",