@venusprotocol/liquidity-hub 2.1.0-dev.4 → 2.1.0-dev.6
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.
- package/README.md +111 -30
- package/contracts/Hub/Hub.sol +71 -63
- package/contracts/adapters/AdapterCentrifuge.sol +5 -0
- package/contracts/adapters/AdapterCoreV1.sol +6 -0
- package/contracts/adapters/AdapterFRV.sol +27 -7
- package/contracts/adapters/AdapterFlux.sol +6 -0
- package/contracts/adapters/AdapterSpokeV1.sol +428 -0
- package/contracts/interfaces/IHub.sol +6 -0
- package/contracts/interfaces/IHubLens.sol +71 -0
- package/contracts/interfaces/IResourceAdapter.sol +23 -7
- package/contracts/interfaces/IYieldGroupBase.sol +6 -1
- package/contracts/interfaces/external/ISpokeComptroller.sol +69 -0
- package/contracts/interfaces/external/IVTokenIsolated.sol +153 -0
- package/contracts/lens/HubCapacityMath.sol +610 -0
- package/contracts/lens/HubLens.sol +311 -0
- package/deployments/bscmainnet/AdapterCentrifuge.json +21 -2
- package/deployments/bscmainnet/AdapterCoreV1.json +21 -2
- package/deployments/bscmainnet/AdapterFRV.json +21 -2
- package/deployments/bscmainnet/AdapterFlux.json +21 -2
- package/deployments/bscmainnet/AdapterSpokeV1.json +339 -0
- package/deployments/bscmainnet/HubImpl.json +16 -3
- package/deployments/bscmainnet/HubLens.json +89 -0
- package/deployments/bscmainnet/SpokeBeacon.json +110 -0
- package/deployments/bscmainnet/SpokeSource_U.json +1083 -0
- package/deployments/bscmainnet/SpokeSource_USDC.json +1083 -0
- package/deployments/bscmainnet/SpokeSource_USDT.json +1083 -0
- package/deployments/bscmainnet/YieldGroupSpokeImpl.json +1083 -0
- package/deployments/bscmainnet_addresses.json +13 -6
- package/deployments/bsctestnet/AdapterCentrifuge.json +21 -2
- package/deployments/bsctestnet/AdapterCoreV1.json +21 -2
- package/deployments/bsctestnet/AdapterFRV.json +21 -2
- package/deployments/bsctestnet/AdapterFlux.json +21 -2
- package/deployments/bsctestnet/AdapterSpokeV1.json +339 -0
- package/deployments/bsctestnet/HubImpl.json +16 -3
- package/deployments/bsctestnet/HubLens.json +89 -0
- package/deployments/bsctestnet/SpokeBeacon.json +110 -0
- package/deployments/bsctestnet/SpokeSource_USDT.json +1083 -0
- package/deployments/bsctestnet/YieldGroupSpokeImpl.json +1083 -0
- package/deployments/bsctestnet_addresses.json +11 -6
- package/package.json +1 -1
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title HubCapacityMath
|
|
6
|
+
* @author Venus
|
|
7
|
+
* @notice Aggregate deposit capacity for a {Hub}, solved against the post-deposit TVL.
|
|
8
|
+
* @dev Pure arithmetic over values already in memory. It never reads a Hub — {HubLens} gathers the
|
|
9
|
+
* inputs and hands them over — so a test can drive it with cap books a live Hub would refuse to
|
|
10
|
+
* accept (`absoluteCap == type(uint256).max`, `percentageCapBps > 10_000`) and still reach the
|
|
11
|
+
* saturation and overflow guards.
|
|
12
|
+
*
|
|
13
|
+
* A YieldGroup's cap is `min(absoluteCap, pctBps × TVL / 10_000)`, so a deposit lifts the very
|
|
14
|
+
* caps it is measured against. This library finds the largest TVL every cap still admits, and
|
|
15
|
+
* returns the distance from today's TVL to it.
|
|
16
|
+
*
|
|
17
|
+
* Write `X` for the post-deposit TVL and, for each YieldGroup `i` in {GroupInput}, `bal_i` for
|
|
18
|
+
* what it holds today, `absBound_i` for the most it could ever hold, and `pct_i` for its
|
|
19
|
+
* percentage cap. What it holds once TVL is `X` is
|
|
20
|
+
*
|
|
21
|
+
* hold_i(X) = max(bal_i, min(absBound_i, pct_i × X / 10_000))
|
|
22
|
+
*
|
|
23
|
+
* Writing `C` for the assets that cannot move — Hub idle, paused YieldGroups, YieldGroups off
|
|
24
|
+
* the deposit queue, any whose views revert — the answer is the largest `X` satisfying
|
|
25
|
+
* `X ≤ C + Σ hold_i(X)`, less `hubTotalAssets`. Derivation and worked example are in the
|
|
26
|
+
* README, under Cap enforcement.
|
|
27
|
+
*
|
|
28
|
+
* Each `hold_i` is flat, then sloped, then flat, with a breakpoint at each turn:
|
|
29
|
+
*
|
|
30
|
+
* hold_i
|
|
31
|
+
* ^
|
|
32
|
+
* | +----------- absBound_i absolute bound binds
|
|
33
|
+
* | /
|
|
34
|
+
* | / percentage cap binds, slope pct_i
|
|
35
|
+
* | +------------+ bal_i balance floor binds
|
|
36
|
+
* +--------+------------+--------------> TVL (X)
|
|
37
|
+
* breakLow_i breakHigh_i
|
|
38
|
+
*
|
|
39
|
+
* Summed over the queue, the corners are every YieldGroup's breakpoints together. Between two
|
|
40
|
+
* adjacent corners no YieldGroup changes which bound it sits on, so the sum is
|
|
41
|
+
* `base + slope × X / 10_000` there and crosses in closed form. {_solveCeiling} sorts the
|
|
42
|
+
* corners, solves each segment, and keeps the largest crossing that lands inside its own one.
|
|
43
|
+
*
|
|
44
|
+
* Never over-reports: the flooring the cascade does per YieldGroup is pre-paid off each segment
|
|
45
|
+
* before it is solved, so `Hub.deposit(HubLens.maxDeposit(hub))` fits.
|
|
46
|
+
*/
|
|
47
|
+
library HubCapacityMath {
|
|
48
|
+
/// @notice BPS denominator (`10_000` = 100%); mirrors `Hub.BPS_DENOMINATOR`.
|
|
49
|
+
uint16 internal constant BPS_DENOMINATOR = 10_000;
|
|
50
|
+
|
|
51
|
+
/// @notice Downward refinement passes before falling back to the pre-deposit room.
|
|
52
|
+
/// @dev Backstop for segment boundaries only — the pre-payment in {_solveCeiling} means the
|
|
53
|
+
/// first check normally passes and none of these run.
|
|
54
|
+
uint256 private constant MAX_REFINEMENTS = 3;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @notice One deposit-queue YieldGroup, as read off the chain.
|
|
58
|
+
* @dev Caller supplies only YieldGroups the cascade would actually route to: registered,
|
|
59
|
+
* unpaused, and answering both views. Everything it leaves out is immovable TVL.
|
|
60
|
+
* @param balance YieldGroup's current `totalAssets()`.
|
|
61
|
+
* @param venueRoom YieldGroup's current `maxDeposit()` — what its venues will still take.
|
|
62
|
+
* @param absoluteCap Hub-configured hard cap for this YieldGroup.
|
|
63
|
+
* @param percentageCapBps Hub-configured percentage cap in BPS; `10_000` disables the
|
|
64
|
+
* percentage dimension (see `IHub.YieldGroupConfig`).
|
|
65
|
+
*/
|
|
66
|
+
struct GroupInput {
|
|
67
|
+
uint256 balance;
|
|
68
|
+
uint256 venueRoom;
|
|
69
|
+
uint256 absoluteCap;
|
|
70
|
+
uint16 percentageCapBps;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @notice One YieldGroup after classification, reduced to the numbers the solve needs.
|
|
75
|
+
* @param balance YieldGroup's current `totalAssets()`.
|
|
76
|
+
* @param absBound `min(absoluteCap, balance + venueRoom)`, saturating on the add.
|
|
77
|
+
* @param breakLow Lowest TVL at which the percentage cap reaches `balance`.
|
|
78
|
+
* @param breakHigh Lowest TVL at which the percentage cap reaches `absBound`.
|
|
79
|
+
* @param pctBps Percentage cap in BPS, or 0 when it can never bind — the 100% sentinel disabled
|
|
80
|
+
* it, or the YieldGroup is pinned at `balance` whatever the TVL.
|
|
81
|
+
*/
|
|
82
|
+
struct CapPoint {
|
|
83
|
+
uint256 balance;
|
|
84
|
+
uint256 absBound;
|
|
85
|
+
uint256 breakLow;
|
|
86
|
+
uint256 breakHigh;
|
|
87
|
+
uint16 pctBps;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @notice One resource of a YieldGroup that has a minimum deposit, in inner deposit-queue order.
|
|
92
|
+
* @dev Such a resource skips a leg below `minDeposit` unless it exactly fills `room` (the tail),
|
|
93
|
+
* so a leg the Hub cap or the queue shrinks after `maxDeposit()` was read can be refused.
|
|
94
|
+
* @param room What the resource will still take.
|
|
95
|
+
* @param minDeposit Smallest leg it accepts short of `room`. `0` = no floor.
|
|
96
|
+
*/
|
|
97
|
+
struct ResourceFloor {
|
|
98
|
+
uint256 room;
|
|
99
|
+
uint256 minDeposit;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================== Internal ==================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @notice Aggregate asset units the deposit cascade would accept right now.
|
|
106
|
+
* @param groups Routable YieldGroups, in deposit-queue order.
|
|
107
|
+
* @param hubTotalAssets Hub's `totalAssets()` — the pre-deposit TVL.
|
|
108
|
+
* @return assets Asset units that could be deposited right now.
|
|
109
|
+
*/
|
|
110
|
+
function capacity(GroupInput[] memory groups, uint256 hubTotalAssets) internal pure returns (uint256 assets) {
|
|
111
|
+
(CapPoint[] memory points, uint256 immovable) = _toPoints(groups, hubTotalAssets);
|
|
112
|
+
return _capacityOn(points, immovable, hubTotalAssets);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @notice The same figure {capacity} reports, split by the YieldGroup that would receive it.
|
|
117
|
+
* @dev Walks `groups` in deposit-queue order handing each one as much as it can hold, exactly as
|
|
118
|
+
* the Hub's cascade does, so `Σ perGroup == total`. Answers "where would my deposit land",
|
|
119
|
+
* which is not the same question as "how much could this YieldGroup take on its own" — a
|
|
120
|
+
* YieldGroup late in the queue reads 0 here whenever the ones before it absorb everything.
|
|
121
|
+
* @param groups Routable YieldGroups, in deposit-queue order.
|
|
122
|
+
* @param hubTotalAssets Hub's `totalAssets()` — the pre-deposit TVL.
|
|
123
|
+
* @return total Asset units that could be deposited right now.
|
|
124
|
+
* @return perGroup Per-YieldGroup share of `total`, aligned index-for-index with `groups`.
|
|
125
|
+
*/
|
|
126
|
+
function capacityPerGroup(GroupInput[] memory groups, uint256 hubTotalAssets)
|
|
127
|
+
internal
|
|
128
|
+
pure
|
|
129
|
+
returns (uint256 total, uint256[] memory perGroup)
|
|
130
|
+
{
|
|
131
|
+
(CapPoint[] memory points, uint256 immovable) = _toPoints(groups, hubTotalAssets);
|
|
132
|
+
total = _capacityOn(points, immovable, hubTotalAssets);
|
|
133
|
+
if (total == 0) return (total, new uint256[](groups.length));
|
|
134
|
+
|
|
135
|
+
perGroup = _splitOn(points, hubTotalAssets, total);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @notice {capacityPerGroup}, less an accrual margin of `balance / marginDivisor` per YieldGroup.
|
|
140
|
+
* @dev One entry point for the whole figure a view can safely promise, so the {CapPoint} set is
|
|
141
|
+
* built once instead of once per step. A deposit settles resource interest before it prices
|
|
142
|
+
* caps (`Hub._accrueFees`); a view cannot, so a Core position is marked off a stale
|
|
143
|
+
* `exchangeRateStored()` and its real room is smaller. The margin is trimmed off the answer
|
|
144
|
+
* rather than added to the mark: a higher mark would also lift the TVL the percentage caps
|
|
145
|
+
* are priced at.
|
|
146
|
+
*
|
|
147
|
+
* Sized on the balance, not on the room, because the interest a deposit settles scales with
|
|
148
|
+
* what the YieldGroup holds.
|
|
149
|
+
* @param groups Routable YieldGroups, in deposit-queue order.
|
|
150
|
+
* @param hubTotalAssets Hub's `totalAssets()`, the pre-deposit TVL.
|
|
151
|
+
* @param marginDivisor Margin denominator, e.g. `10_000` for 0.01% of each balance. Must be
|
|
152
|
+
* non-zero.
|
|
153
|
+
* @return total Asset units a view can promise, margin already trimmed.
|
|
154
|
+
* @return perGroup Per-YieldGroup share of `total`, aligned index-for-index with `groups`.
|
|
155
|
+
*/
|
|
156
|
+
function trimmedCapacity(GroupInput[] memory groups, uint256 hubTotalAssets, uint256 marginDivisor)
|
|
157
|
+
internal
|
|
158
|
+
pure
|
|
159
|
+
returns (uint256 total, uint256[] memory perGroup)
|
|
160
|
+
{
|
|
161
|
+
(CapPoint[] memory points, uint256 immovable) = _toPoints(groups, hubTotalAssets);
|
|
162
|
+
total = _trimmedOn(points, immovable, hubTotalAssets, marginDivisor);
|
|
163
|
+
perGroup = _splitOn(points, hubTotalAssets, total);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @notice {trimmedCapacity}'s total, trimmed once more, for a figure quoted in shares.
|
|
168
|
+
* @dev `mint` prices the shares at a share price the deposit's own interest settlement raises,
|
|
169
|
+
* so the same share count converts to more assets than it does in this view. One more
|
|
170
|
+
* margin covers that, on the same {CapPoint} set as the first.
|
|
171
|
+
* @param groups Routable YieldGroups, in deposit-queue order.
|
|
172
|
+
* @param hubTotalAssets Hub's `totalAssets()`, the pre-deposit TVL.
|
|
173
|
+
* @param marginDivisor Margin denominator, e.g. `10_000` for 0.01%. Must be non-zero.
|
|
174
|
+
* @return total Asset units a share-denominated view can promise.
|
|
175
|
+
*/
|
|
176
|
+
function doubleTrimmedCapacity(GroupInput[] memory groups, uint256 hubTotalAssets, uint256 marginDivisor)
|
|
177
|
+
internal
|
|
178
|
+
pure
|
|
179
|
+
returns (uint256 total)
|
|
180
|
+
{
|
|
181
|
+
(CapPoint[] memory points, uint256 immovable) = _toPoints(groups, hubTotalAssets);
|
|
182
|
+
total = _trimmedOn(points, immovable, hubTotalAssets, marginDivisor);
|
|
183
|
+
return _feasibleOn(points, hubTotalAssets, total - total / marginDivisor);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// @notice Room the cascade would still leave at `tvl`, floored per YieldGroup exactly as it
|
|
187
|
+
/// floors. Lets a caller check that a figure smaller than {capacity} is placeable: every
|
|
188
|
+
/// cap shrinks with the TVL, so a smaller deposit does not automatically fit.
|
|
189
|
+
/// @param groups Routable YieldGroups, in deposit-queue order.
|
|
190
|
+
/// @param hubTotalAssets Hub's `totalAssets()` — the pre-deposit TVL.
|
|
191
|
+
/// @param tvl TVL to measure the caps at.
|
|
192
|
+
/// @return room Asset units the cascade would accept at `tvl`.
|
|
193
|
+
function roomAt(GroupInput[] memory groups, uint256 hubTotalAssets, uint256 tvl)
|
|
194
|
+
internal
|
|
195
|
+
pure
|
|
196
|
+
returns (uint256 room)
|
|
197
|
+
{
|
|
198
|
+
(CapPoint[] memory points,) = _toPoints(groups, hubTotalAssets);
|
|
199
|
+
return _roomAt(points, tvl);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @notice Room values worth offering the solve for one YieldGroup with floored resources,
|
|
204
|
+
* largest first: `venueRoom`, then each queue-prefix sum of room below it, then 0.
|
|
205
|
+
* @dev A leg equal to a prefix sum fills every resource in that prefix exactly, so each one
|
|
206
|
+
* takes its whole room and no floor applies. Between prefix sums a leg can land under the
|
|
207
|
+
* next resource's floor, so these are the only edges the caller needs to try.
|
|
208
|
+
* @param floors The YieldGroup's resources, in inner deposit-queue order.
|
|
209
|
+
* @param venueRoom The YieldGroup's `maxDeposit()`.
|
|
210
|
+
* @return values Candidate rooms, non-increasing, ending in 0.
|
|
211
|
+
*/
|
|
212
|
+
function roomCandidates(ResourceFloor[] memory floors, uint256 venueRoom)
|
|
213
|
+
internal
|
|
214
|
+
pure
|
|
215
|
+
returns (uint256[] memory values)
|
|
216
|
+
{
|
|
217
|
+
uint256 len = floors.length;
|
|
218
|
+
values = new uint256[](len + 1);
|
|
219
|
+
uint256 prefix;
|
|
220
|
+
for (uint256 k; k < len; ++k) {
|
|
221
|
+
prefix = _satAdd(prefix, floors[k].room);
|
|
222
|
+
values[len - 1 - k] = prefix < venueRoom ? prefix : venueRoom;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @notice Dry run of `Hub._routeDeposit(amount)`: each YieldGroup in queue order gets
|
|
228
|
+
* `min(remaining, cap room, venueRoom)`, a floored one places it through its resources
|
|
229
|
+
* the way `YieldGroupBase.deposit` does, and whatever it skips moves on.
|
|
230
|
+
* @dev Caps are rounded up and priced at `hubTotalAssets + amount`, as `Hub._effectiveCap` does.
|
|
231
|
+
* @param groups Routable YieldGroups, in deposit-queue order, with their real `venueRoom`.
|
|
232
|
+
* @param floors Per YieldGroup, its resources if any has a floor, else empty.
|
|
233
|
+
* @param hubTotalAssets Hub's `totalAssets()`, the pre-deposit TVL.
|
|
234
|
+
* @param amount Deposit to route.
|
|
235
|
+
* @return left What no YieldGroup placed. Non-zero means the Hub reverts `HubCapacityExceeded`.
|
|
236
|
+
* @return placed Per-YieldGroup amount placed, aligned with `groups`.
|
|
237
|
+
* @return short Per YieldGroup, whether its resources skipped part of the leg it was sent.
|
|
238
|
+
*/
|
|
239
|
+
function route(GroupInput[] memory groups, ResourceFloor[][] memory floors, uint256 hubTotalAssets, uint256 amount)
|
|
240
|
+
internal
|
|
241
|
+
pure
|
|
242
|
+
returns (uint256 left, uint256[] memory placed, bool[] memory short)
|
|
243
|
+
{
|
|
244
|
+
uint256 n = groups.length;
|
|
245
|
+
placed = new uint256[](n);
|
|
246
|
+
short = new bool[](n);
|
|
247
|
+
uint256 tvl = _satAdd(hubTotalAssets, amount);
|
|
248
|
+
left = amount;
|
|
249
|
+
|
|
250
|
+
for (uint256 i; i < n && left > 0; ++i) {
|
|
251
|
+
GroupInput memory g = groups[i];
|
|
252
|
+
uint256 cap = _hubCap(g.absoluteCap, g.percentageCapBps, tvl);
|
|
253
|
+
uint256 room = cap > g.balance ? cap - g.balance : 0;
|
|
254
|
+
if (g.venueRoom < room) room = g.venueRoom;
|
|
255
|
+
uint256 sent = left < room ? left : room;
|
|
256
|
+
|
|
257
|
+
uint256 took = floors[i].length == 0 ? sent : _placeThrough(floors[i], sent);
|
|
258
|
+
placed[i] = took;
|
|
259
|
+
short[i] = took < sent;
|
|
260
|
+
left -= took;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================== Private — classification ==================
|
|
265
|
+
|
|
266
|
+
/// @dev Reduce each {GroupInput} to its {CapPoint}, plus the TVL that cannot move: Hub idle,
|
|
267
|
+
/// paused YieldGroups, ones outside the deposit queue, and any whose views reverted.
|
|
268
|
+
function _toPoints(GroupInput[] memory groups, uint256 hubTotalAssets)
|
|
269
|
+
private
|
|
270
|
+
pure
|
|
271
|
+
returns (CapPoint[] memory points, uint256 immovable)
|
|
272
|
+
{
|
|
273
|
+
uint256 len = groups.length;
|
|
274
|
+
points = new CapPoint[](len);
|
|
275
|
+
uint256 tracked;
|
|
276
|
+
|
|
277
|
+
for (uint256 i; i < len; ++i) {
|
|
278
|
+
GroupInput memory g = groups[i];
|
|
279
|
+
|
|
280
|
+
uint256 absBound = _satAdd(g.balance, g.venueRoom);
|
|
281
|
+
if (g.absoluteCap < absBound) absBound = g.absoluteCap;
|
|
282
|
+
|
|
283
|
+
uint16 pctBps = g.percentageCapBps;
|
|
284
|
+
uint256 breakLow;
|
|
285
|
+
uint256 breakHigh;
|
|
286
|
+
if (pctBps == 0 || g.balance >= absBound) {
|
|
287
|
+
// Pinned at `balance` for every TVL. Checked BEFORE the 100% sentinel: a YieldGroup
|
|
288
|
+
// holding more than its cap still holds it — a deposit cannot claw it back — so it
|
|
289
|
+
// must count as `balance`, not `absBound`, or the solve understates reachable TVL.
|
|
290
|
+
pctBps = 0;
|
|
291
|
+
breakLow = type(uint256).max;
|
|
292
|
+
breakHigh = type(uint256).max;
|
|
293
|
+
} else if (pctBps == BPS_DENOMINATOR) {
|
|
294
|
+
// Percentage dimension disabled by the 100% sentinel: only `absBound` ever binds,
|
|
295
|
+
// at every TVL. Both breakpoints sit at 0 so the YieldGroup is always absolute-bound.
|
|
296
|
+
pctBps = 0;
|
|
297
|
+
} else {
|
|
298
|
+
breakLow = _tvlReaching(g.balance, pctBps);
|
|
299
|
+
breakHigh = _tvlReaching(absBound, pctBps);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
points[i] = CapPoint({
|
|
303
|
+
balance: g.balance, absBound: absBound, breakLow: breakLow, breakHigh: breakHigh, pctBps: pctBps
|
|
304
|
+
});
|
|
305
|
+
tracked = _satAdd(tracked, g.balance);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
immovable = hubTotalAssets > tracked ? hubTotalAssets - tracked : 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============================== Private over a built point set ============
|
|
312
|
+
|
|
313
|
+
/// @dev {capacity} once the {CapPoint} set is in hand.
|
|
314
|
+
function _capacityOn(CapPoint[] memory points, uint256 immovable, uint256 hubTotalAssets)
|
|
315
|
+
private
|
|
316
|
+
pure
|
|
317
|
+
returns (uint256 assets)
|
|
318
|
+
{
|
|
319
|
+
if (points.length == 0) return 0;
|
|
320
|
+
|
|
321
|
+
uint256 ceiling = _solveCeiling(points, immovable, hubTotalAssets);
|
|
322
|
+
assets = ceiling > hubTotalAssets ? ceiling - hubTotalAssets : 0;
|
|
323
|
+
|
|
324
|
+
// The cascade will price its caps at `hubTotalAssets + assets`. Confirm that TVL really
|
|
325
|
+
// does leave `assets` of room, and walk the figure down if the closed form rounded high.
|
|
326
|
+
for (uint256 i; i < MAX_REFINEMENTS; ++i) {
|
|
327
|
+
uint256 room = _roomAt(points, hubTotalAssets + assets);
|
|
328
|
+
if (room >= assets) return assets;
|
|
329
|
+
assets = room;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Unreached in practice. The room at the pre-deposit TVL is always feasible, because every
|
|
333
|
+
// cap is non-decreasing in TVL and depositing only raises it.
|
|
334
|
+
return _roomAt(points, hubTotalAssets);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// @dev {capacity} less the accrual margin, once the {CapPoint} set is in hand. Trimmed per
|
|
338
|
+
/// YieldGroup, off the room that YieldGroup would actually receive, and each one floors at
|
|
339
|
+
/// 0 on its own: a YieldGroup whose room is smaller than its own margin reads as full.
|
|
340
|
+
function _trimmedOn(CapPoint[] memory points, uint256 immovable, uint256 hubTotalAssets, uint256 marginDivisor)
|
|
341
|
+
private
|
|
342
|
+
pure
|
|
343
|
+
returns (uint256 total)
|
|
344
|
+
{
|
|
345
|
+
uint256 untrimmed = _capacityOn(points, immovable, hubTotalAssets);
|
|
346
|
+
if (untrimmed == 0) return 0;
|
|
347
|
+
|
|
348
|
+
uint256[] memory split = _splitOn(points, hubTotalAssets, untrimmed);
|
|
349
|
+
for (uint256 i; i < split.length; ++i) {
|
|
350
|
+
uint256 margin = points[i].balance / marginDivisor;
|
|
351
|
+
total += split[i] > margin ? split[i] - margin : 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return _feasibleOn(points, hubTotalAssets, total);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/// @dev Split `amount` across the YieldGroups the way the cascade would place it, with every
|
|
358
|
+
/// cap priced at `hubTotalAssets + amount`, once the {CapPoint} set is in hand.
|
|
359
|
+
function _splitOn(CapPoint[] memory points, uint256 hubTotalAssets, uint256 amount)
|
|
360
|
+
private
|
|
361
|
+
pure
|
|
362
|
+
returns (uint256[] memory perGroup)
|
|
363
|
+
{
|
|
364
|
+
perGroup = new uint256[](points.length);
|
|
365
|
+
uint256 postTvl = hubTotalAssets + amount;
|
|
366
|
+
uint256 remaining = amount;
|
|
367
|
+
|
|
368
|
+
for (uint256 i; i < points.length && remaining > 0; ++i) {
|
|
369
|
+
uint256 held = _holdAt(points[i], postTvl);
|
|
370
|
+
uint256 balance = points[i].balance;
|
|
371
|
+
uint256 room = held > balance ? held - balance : 0;
|
|
372
|
+
if (room > remaining) room = remaining;
|
|
373
|
+
perGroup[i] = room;
|
|
374
|
+
remaining -= room;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/// @dev Walk `total` down to something the cascade would actually place. Trimming breaks the
|
|
379
|
+
/// fixed point {_solveCeiling} found: every cap scales with the TVL, so a smaller deposit
|
|
380
|
+
/// buys smaller caps, and where the percentage caps sum to ~100% each one's flooring sheds
|
|
381
|
+
/// a wei the smaller figure no longer pre-pays.
|
|
382
|
+
function _feasibleOn(CapPoint[] memory points, uint256 hubTotalAssets, uint256 total)
|
|
383
|
+
private
|
|
384
|
+
pure
|
|
385
|
+
returns (uint256)
|
|
386
|
+
{
|
|
387
|
+
for (uint256 i; i < MAX_REFINEMENTS; ++i) {
|
|
388
|
+
uint256 room = _roomAt(points, hubTotalAssets + total);
|
|
389
|
+
if (room >= total) return total;
|
|
390
|
+
|
|
391
|
+
// Snap the post-deposit TVL down to a multiple of `BPS_DENOMINATOR`, where every
|
|
392
|
+
// percentage cap divides exactly and nothing is shed. Only reached when the plain room
|
|
393
|
+
// is short, so a book without that rounding keeps its exact figure.
|
|
394
|
+
uint256 postTvl = ((hubTotalAssets + room) / BPS_DENOMINATOR) * BPS_DENOMINATOR;
|
|
395
|
+
total = postTvl > hubTotalAssets ? postTvl - hubTotalAssets : room;
|
|
396
|
+
}
|
|
397
|
+
return total;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============================== Private — the solve =======================
|
|
401
|
+
|
|
402
|
+
/// @dev Largest post-deposit TVL every cap admits. Walks the segments between breakpoints and
|
|
403
|
+
/// keeps the highest crossing that lands inside its own segment. Seeded at `floorTvl`, the
|
|
404
|
+
/// pre-deposit TVL, which always satisfies the inequality: every YieldGroup already holds
|
|
405
|
+
/// what the sum credits it with there.
|
|
406
|
+
function _solveCeiling(CapPoint[] memory points, uint256 immovable, uint256 floorTvl)
|
|
407
|
+
private
|
|
408
|
+
pure
|
|
409
|
+
returns (uint256 ceiling)
|
|
410
|
+
{
|
|
411
|
+
(uint256[] memory bounds, uint256 m) = _sortedBounds(points, floorTvl);
|
|
412
|
+
|
|
413
|
+
ceiling = floorTvl;
|
|
414
|
+
for (uint256 s; s < m; ++s) {
|
|
415
|
+
uint256 low = bounds[s];
|
|
416
|
+
uint256 high = s + 1 < m ? bounds[s + 1] : type(uint256).max;
|
|
417
|
+
(uint256 base, uint256 slopeBps, uint256 pctBoundCount) = _segmentAt(points, immovable, low);
|
|
418
|
+
if (slopeBps >= BPS_DENOMINATOR) {
|
|
419
|
+
// Slope >= 100%: every TVL in the segment fits, so take its top edge, snapped to a
|
|
420
|
+
// multiple of BPS_DENOMINATOR where the per-group floors lose nothing.
|
|
421
|
+
if (high != type(uint256).max) {
|
|
422
|
+
uint256 edge = (high / BPS_DENOMINATOR) * BPS_DENOMINATOR;
|
|
423
|
+
if (edge >= low && edge > ceiling) ceiling = edge;
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Crossing below `low` is not in this segment. Above `high`, the whole segment fits,
|
|
429
|
+
// so its top edge is the candidate.
|
|
430
|
+
uint256 exact = _crossing(base, slopeBps);
|
|
431
|
+
if (exact < low) continue;
|
|
432
|
+
if (exact > high) {
|
|
433
|
+
if (high > low) ceiling = _raiseIfFits(points, floorTvl, ceiling, high - 1);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Per-group flooring can leave `exact` a wei short. Test it before pre-paying, so a
|
|
438
|
+
// crossing on `low` is not pushed into the segment below.
|
|
439
|
+
if (exact > ceiling && exact - floorTvl <= _roomAt(points, exact)) {
|
|
440
|
+
ceiling = exact;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Pre-pay the flooring loss: one wei per percentage-bound YieldGroup.
|
|
445
|
+
if (base <= pctBoundCount) continue;
|
|
446
|
+
uint256 crossing = _crossing(base - pctBoundCount, slopeBps);
|
|
447
|
+
ceiling = _raiseIfFits(points, floorTvl, ceiling, crossing);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/// @dev `floorTvl` plus every finite breakpoint above it, sorted ascending; `m` is the count used.
|
|
452
|
+
function _sortedBounds(CapPoint[] memory points, uint256 floorTvl)
|
|
453
|
+
private
|
|
454
|
+
pure
|
|
455
|
+
returns (uint256[] memory bounds, uint256 m)
|
|
456
|
+
{
|
|
457
|
+
uint256 n = points.length;
|
|
458
|
+
bounds = new uint256[](2 * n + 1);
|
|
459
|
+
|
|
460
|
+
bounds[m] = floorTvl;
|
|
461
|
+
++m;
|
|
462
|
+
for (uint256 i; i < n; ++i) {
|
|
463
|
+
uint256 low = points[i].breakLow;
|
|
464
|
+
uint256 high = points[i].breakHigh;
|
|
465
|
+
if (low > floorTvl && low != type(uint256).max) {
|
|
466
|
+
bounds[m] = low;
|
|
467
|
+
++m;
|
|
468
|
+
}
|
|
469
|
+
if (high > floorTvl && high != type(uint256).max) {
|
|
470
|
+
bounds[m] = high;
|
|
471
|
+
++m;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Insertion sort: `m ≤ 2n + 1` and a Hub carries a handful of YieldGroups.
|
|
476
|
+
for (uint256 i = 1; i < m; ++i) {
|
|
477
|
+
uint256 v = bounds[i];
|
|
478
|
+
uint256 j = i;
|
|
479
|
+
while (j > 0 && bounds[j - 1] > v) {
|
|
480
|
+
bounds[j] = bounds[j - 1];
|
|
481
|
+
--j;
|
|
482
|
+
}
|
|
483
|
+
bounds[j] = v;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/// @dev `candidate`, walked down like {capacity}, if the cascade admits it; else `ceiling`.
|
|
488
|
+
/// An unchecked candidate a wei too high would replace a ceiling that fits.
|
|
489
|
+
function _raiseIfFits(CapPoint[] memory points, uint256 floorTvl, uint256 ceiling, uint256 candidate)
|
|
490
|
+
private
|
|
491
|
+
pure
|
|
492
|
+
returns (uint256)
|
|
493
|
+
{
|
|
494
|
+
for (uint256 i; i < MAX_REFINEMENTS && candidate > ceiling; ++i) {
|
|
495
|
+
uint256 fits = _satAdd(floorTvl, _roomAt(points, candidate));
|
|
496
|
+
if (fits >= candidate) return candidate;
|
|
497
|
+
candidate = fits;
|
|
498
|
+
}
|
|
499
|
+
return ceiling;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/// @dev Constant, slope and percentage-bound count on the segment starting at `tvl`, where
|
|
503
|
+
/// each YieldGroup's classification is constant. `pctBoundCount` counts the separate floors
|
|
504
|
+
/// the cascade will take, one wei each, which is what the caller pre-pays off `base`.
|
|
505
|
+
function _segmentAt(CapPoint[] memory points, uint256 immovable, uint256 tvl)
|
|
506
|
+
private
|
|
507
|
+
pure
|
|
508
|
+
returns (uint256 base, uint256 slopeBps, uint256 pctBoundCount)
|
|
509
|
+
{
|
|
510
|
+
base = immovable;
|
|
511
|
+
uint256 n = points.length;
|
|
512
|
+
for (uint256 i; i < n; ++i) {
|
|
513
|
+
CapPoint memory p = points[i];
|
|
514
|
+
if (tvl >= p.breakHigh) {
|
|
515
|
+
base = _satAdd(base, p.absBound);
|
|
516
|
+
} else if (tvl < p.breakLow) {
|
|
517
|
+
base = _satAdd(base, p.balance);
|
|
518
|
+
} else {
|
|
519
|
+
slopeBps += p.pctBps;
|
|
520
|
+
++pctBoundCount;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/// @dev Solve `X = base + slopeBps × X / 10_000`, rounded down so the result stays admissible
|
|
526
|
+
/// on its segment. Returns 0 when `base` cannot be scaled without overflow, which reads as
|
|
527
|
+
/// "no crossing here" rather than reverting the whole view. Caller guarantees slope < 100%.
|
|
528
|
+
function _crossing(uint256 base, uint256 slopeBps) private pure returns (uint256 crossing) {
|
|
529
|
+
if (base > type(uint256).max / BPS_DENOMINATOR) return 0;
|
|
530
|
+
crossing = (base * BPS_DENOMINATOR) / (BPS_DENOMINATOR - slopeBps);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ============================== Private — exact room ======================
|
|
534
|
+
|
|
535
|
+
/// @dev Room at `tvl`, flooring each percentage cap. The Hub rounds it up, so this is never
|
|
536
|
+
/// above the room the cascade gives.
|
|
537
|
+
function _roomAt(CapPoint[] memory points, uint256 tvl) private pure returns (uint256 room) {
|
|
538
|
+
uint256 n = points.length;
|
|
539
|
+
for (uint256 i; i < n; ++i) {
|
|
540
|
+
uint256 held = _holdAt(points[i], tvl);
|
|
541
|
+
uint256 balance = points[i].balance;
|
|
542
|
+
if (held > balance) room = _satAdd(room, held - balance);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/// @dev What one YieldGroup holds once TVL is `tvl`, i.e. `hold_i` of the header:
|
|
547
|
+
/// `max(balance, min(absBound, pctBps × tvl / 10_000))`. The breakpoints already say which
|
|
548
|
+
/// of the three cases applies, so nothing is compared here.
|
|
549
|
+
function _holdAt(CapPoint memory p, uint256 tvl) private pure returns (uint256 held) {
|
|
550
|
+
if (tvl >= p.breakHigh) return p.absBound;
|
|
551
|
+
if (tvl < p.breakLow) return p.balance;
|
|
552
|
+
held = _mulBpsDown(tvl, p.pctBps);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ============================== Private — floored routing =================
|
|
556
|
+
|
|
557
|
+
/// @dev What a YieldGroup places of `amount` across `floors`, mirroring
|
|
558
|
+
/// `YieldGroupBase._tryDepositOneResource`: a leg under the floor that is not the whole room
|
|
559
|
+
/// is skipped and carried to the next resource.
|
|
560
|
+
function _placeThrough(ResourceFloor[] memory floors, uint256 amount) private pure returns (uint256) {
|
|
561
|
+
uint256 remaining = amount;
|
|
562
|
+
for (uint256 k; k < floors.length && remaining > 0; ++k) {
|
|
563
|
+
uint256 room = floors[k].room;
|
|
564
|
+
uint256 leg = remaining < room ? remaining : room;
|
|
565
|
+
if (leg == 0 || (leg < room && leg < floors[k].minDeposit)) continue;
|
|
566
|
+
remaining -= leg;
|
|
567
|
+
}
|
|
568
|
+
return amount - remaining;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/// @dev `Hub._effectiveCap`: the percentage cap rounded up, the 100% sentinel disabling it.
|
|
572
|
+
/// Saturates instead of overflowing on a TVL no Hub can hold.
|
|
573
|
+
function _hubCap(uint256 absoluteCap, uint16 pctBps, uint256 tvl) private pure returns (uint256) {
|
|
574
|
+
if (pctBps == BPS_DENOMINATOR) return absoluteCap;
|
|
575
|
+
if (pctBps != 0 && tvl > (type(uint256).max - BPS_DENOMINATOR) / pctBps) return absoluteCap;
|
|
576
|
+
uint256 pctCap = (tvl * pctBps + BPS_DENOMINATOR - 1) / BPS_DENOMINATOR;
|
|
577
|
+
return pctCap < absoluteCap ? pctCap : absoluteCap;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ============================== Private — arithmetic ======================
|
|
581
|
+
|
|
582
|
+
/// @dev `⌈target × 10_000 / pctBps⌉`, rounded up so the breakpoint is exact against the floored
|
|
583
|
+
/// cap the cascade computes. `type(uint256).max` when out of range.
|
|
584
|
+
function _tvlReaching(uint256 target, uint16 pctBps) private pure returns (uint256 tvl) {
|
|
585
|
+
if (pctBps == 0) return type(uint256).max;
|
|
586
|
+
if (target > type(uint256).max / BPS_DENOMINATOR) return type(uint256).max;
|
|
587
|
+
uint256 numerator = target * BPS_DENOMINATOR;
|
|
588
|
+
tvl = numerator / pctBps;
|
|
589
|
+
if (numerator % pctBps != 0) ++tvl;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/// @dev `x × bps / 10_000`, rounded down, without overflowing on a large `x`. Saturates at
|
|
593
|
+
/// `type(uint256).max`, which only a `bps` above 10_000 can reach.
|
|
594
|
+
function _mulBpsDown(uint256 x, uint256 bps) private pure returns (uint256 scaled) {
|
|
595
|
+
if (bps == 0) return 0;
|
|
596
|
+
if (x <= type(uint256).max / bps) return (x * bps) / BPS_DENOMINATOR;
|
|
597
|
+
uint256 whole = x / BPS_DENOMINATOR;
|
|
598
|
+
if (whole > type(uint256).max / bps) return type(uint256).max;
|
|
599
|
+
scaled = _satAdd(whole * bps, ((x % BPS_DENOMINATOR) * bps) / BPS_DENOMINATOR);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/// @dev Saturating add: a YieldGroup may advertise an unbounded `maxDeposit()`, and that must
|
|
603
|
+
/// not be able to brick the capacity view.
|
|
604
|
+
function _satAdd(uint256 a, uint256 b) private pure returns (uint256 sum) {
|
|
605
|
+
unchecked {
|
|
606
|
+
sum = a + b;
|
|
607
|
+
if (sum < a) sum = type(uint256).max;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|