@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
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Venus Liquidity Hub
|
|
2
2
|
|
|
3
|
-
Per-asset ERC-4626 allocator vault. A lender deposits a single asset, the Hub routes it across Venus yield families (Core, Flux, FRV) under a governance-set policy, and returns a yield-bearing share token. Yield accrues through a **rising exchange rate** (one share = X underlying), never by rebasing — one Hub per asset, no cross-asset coupling.
|
|
3
|
+
Per-asset ERC-4626 allocator vault. A lender deposits a single asset, the Hub routes it across Venus yield families (Core, Flux, FRV, Spoke) under a governance-set policy, and returns a yield-bearing share token. Yield accrues through a **rising exchange rate** (one share = X underlying), never by rebasing — one Hub per asset, no cross-asset coupling.
|
|
4
4
|
|
|
5
|
-
> **Scope.** v1 ships on BNB Chain with three yield families — **Core**, **Flux**, **FRV**.
|
|
5
|
+
> **Scope.** v1 ships on BNB Chain with three yield families — **Core**, **Flux**, **FRV**. **Spoke** (the liquidity side of hub-funded isolated pools) is built and follows once its markets are listed; RWA (Ondo / BUIDL) is a documented future YieldGroup. The fee machinery is fully implemented but launches at `0/0`.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -25,53 +25,60 @@ flowchart TB
|
|
|
25
25
|
|
|
26
26
|
Hub["Hub_USDT (ERC-4626, beacon proxy)<br/>dual caps · fees · per-tx cap"]:::hub
|
|
27
27
|
|
|
28
|
-
Hub --> OuterQ["outer queue (per direction)<br/>[YieldGroup_Core, YieldGroup_Flux, YieldGroup_FRV]"]:::queue
|
|
28
|
+
Hub --> OuterQ["outer queue (per direction)<br/>[YieldGroup_Core, YieldGroup_Flux, YieldGroup_FRV, YieldGroup_Spoke]"]:::queue
|
|
29
29
|
|
|
30
30
|
OuterQ --> CoreYG["YieldGroup_Core_USDT"]:::src
|
|
31
31
|
OuterQ --> FluxYG["YieldGroup_Flux_USDT"]:::src
|
|
32
32
|
OuterQ --> FrvYG["YieldGroup_FRV_USDT"]:::src
|
|
33
|
+
OuterQ --> SpokeYG["YieldGroup_Spoke_USDT"]:::src
|
|
33
34
|
|
|
34
35
|
CoreYG --> CoreInner["inner queue [vUSDT_v1, ...]"]:::queue
|
|
35
36
|
FluxYG --> FluxInner["inner queue [fUSDT, ...]"]:::queue
|
|
36
37
|
FrvYG --> FrvInner["inner queue [FRV_A, FRV_B, ...]"]:::queue
|
|
38
|
+
SpokeYG --> SpokeInner["inner queue [vUSDT_poolA, vUSDT_poolB, ...]"]:::queue
|
|
37
39
|
|
|
38
40
|
CoreInner -->|"delegatecall"| ACore["AdapterCoreV1<br/>(stateless singleton)"]:::adapter
|
|
39
41
|
FluxInner -->|"delegatecall"| AFlux["AdapterFlux<br/>(stateless singleton)"]:::adapter
|
|
40
42
|
FrvInner -->|"delegatecall"| AFrv["AdapterFRV<br/>(stateless singleton)"]:::adapter
|
|
43
|
+
SpokeInner -->|"delegatecall"| ASpoke["AdapterSpokeV1<br/>(stateless singleton)"]:::adapter
|
|
41
44
|
|
|
42
45
|
ACore --> RCore["vUSDT (Venus Core vToken)"]:::resource
|
|
43
46
|
AFlux --> RFlux["fUSDT (Fluid fToken)"]:::resource
|
|
44
47
|
AFrv --> RFrv["FRV vault (fixed-rate, lifecycle)"]:::resource
|
|
48
|
+
ASpoke --> RSpoke["vUSDT (hub-funded spoke pool,<br/>liquidity side)"]:::resource
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
### Key contracts
|
|
48
52
|
|
|
49
|
-
| Contract | Role
|
|
50
|
-
| -------------------------------------------- |
|
|
51
|
-
| **Hub** | ERC-4626 entry point (beacon proxy). Routes deposits/withdrawals through the outer queue of YieldGroups, manages dual caps, the per-tx withdrawal cap, management + performance fees, and multi-level pause. Registry/queue logic is delegatecall-linked into **HubAdminLib** to stay under the 24 KB bytecode limit.
|
|
52
|
-
| **YieldGroupCore** | `IYieldGroupBase` over Venus **Core-pool vTokens** (`mint` / `redeemUnderlying`). Optional per-resource deposit caps. Grosses up redeems for a Comptroller treasury fee if one is ever enabled.
|
|
53
|
-
| **YieldGroupFlux** | `IYieldGroupBase` over **Fluid Lending fTokens** (ERC-4626 shares). No redeem-time fee. Per-resource caps are the primary deposit control (Fluid's protocol `maxDeposit` is effectively unbounded).
|
|
54
|
-
| **YieldGroupFRV** | `IYieldGroupBase` over Venus **Fixed-Rate Vaults** — ERC-4626 with an 11-state lifecycle. Pokes `updateVaultState()` before every routing decision; deposits only while Fundraising, withdrawals only when terminal.
|
|
55
|
-
| **
|
|
56
|
-
| **
|
|
57
|
-
| **
|
|
58
|
-
| **
|
|
53
|
+
| Contract | Role |
|
|
54
|
+
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| **Hub** | ERC-4626 entry point (beacon proxy). Routes deposits/withdrawals through the outer queue of YieldGroups, manages dual caps, the per-tx withdrawal cap, management + performance fees, and multi-level pause. Registry/queue logic is delegatecall-linked into **HubAdminLib** to stay under the 24 KB bytecode limit. Its `maxDeposit` / `maxMint` return `type(uint256).max`; caps are enforced by the routing cascade and the real capacity figure lives on **HubLens**. |
|
|
56
|
+
| **YieldGroupCore** | `IYieldGroupBase` over Venus **Core-pool vTokens** (`mint` / `redeemUnderlying`). Optional per-resource deposit caps. Grosses up redeems for a Comptroller treasury fee if one is ever enabled. |
|
|
57
|
+
| **YieldGroupFlux** | `IYieldGroupBase` over **Fluid Lending fTokens** (ERC-4626 shares). No redeem-time fee. Per-resource caps are the primary deposit control (Fluid's protocol `maxDeposit` is effectively unbounded). |
|
|
58
|
+
| **YieldGroupFRV** | `IYieldGroupBase` over Venus **Fixed-Rate Vaults** — ERC-4626 with an 11-state lifecycle. Pokes `updateVaultState()` before every routing decision; deposits only while Fundraising, withdrawals only when terminal. |
|
|
59
|
+
| **HubLens** | Stateless, immutable, unowned read surface — no storage, no proxy, no registration. Takes the Hub as a call parameter, so one deployment serves every Hub. Exposes the deposit-capacity solve the Hub no longer runs (`maxDeposit`, `maxMint`, `depositCapacityBreakdown`) plus the Hub-level blended APY (`spotAPYBps`). Frontends and integrators size deposits from here. |
|
|
60
|
+
| **AdapterCoreV1 / AdapterFlux / AdapterFRV** | Stateless singletons — zero storage, only immutables. One deployment per ABI family serves every YieldGroup. Mutating calls run via **delegatecall** so receipt tokens land on the YieldGroup; direct calls revert (`onlyDelegateCall`). |
|
|
61
|
+
| **Migrator** | Immutable, permissionless periphery (no proxy/beacon), one shared deployment per chain. One-click migration of a Venus Core position into a Hub: pulls the caller's vToken, redeems it to the underlying, deposits into the matching Hub, and mints shares to the caller — atomic, with a `minShares` slippage guard. A separate `migrateFromCoreBNB` path wraps a redeemed vBNB position to WBNB before depositing into the WBNB Hub. |
|
|
62
|
+
| **IYieldGroupBase** | Hub↔YieldGroup boundary. `deposit` / `withdraw` (queue) + `depositResource` / `withdrawResource` (targeted, reallocate-only) + `totalAssets` / `maxDeposit` / `maxWithdraw` / `spotAPYBps`. |
|
|
63
|
+
| **IResourceAdapter** | YieldGroup↔adapter boundary. Mutating fns delegatecalled; view fns take an explicit `holder`. `validateRegistration` rejects resources with an unmodeled per-redeem fee. |
|
|
59
64
|
|
|
60
65
|
---
|
|
61
66
|
|
|
62
67
|
> **Terminology.** A _YieldGroup_ is the grouping layer: the Hub depends only on the `IYieldGroupBase` interface and never reaches past a YieldGroup into the underlying market. A _Resource_ is one underlying market or vault held inside a YieldGroup.
|
|
63
68
|
|
|
64
|
-
## The
|
|
69
|
+
## The four yield families
|
|
65
70
|
|
|
66
|
-
| | **Core** | **Flux** | **FRV** |
|
|
67
|
-
| ------------------------ | -------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------ |
|
|
68
|
-
| Underlying protocol | Venus Core lending | Fluid Lending | Venus Fixed-Rate Vaults |
|
|
69
|
-
| Resource / receipt token | vToken (Compound-style) | fToken (ERC-4626 share) | FRV vault share (ERC-4626) |
|
|
70
|
-
| Deposit / withdraw call | `mint` / `redeemUnderlying` | `deposit` / `withdraw(assets)` | `deposit` / `withdraw(assets)` |
|
|
71
|
-
| Redeem-time pool fee | only if Comptroller `treasuryPercent` is later enabled (grossed-up); registration blocked while non-zero | none (Fluid's cut is in the supply rate) | none (reserve factor taken once at settlement) |
|
|
72
|
-
| Spot APY source | `supplyRatePerBlock` × `blocksPerYear` | Fluid `LendingResolver` (pre-annualised APR) | vault `fixedAPY` (only while Fundraising / Lock) |
|
|
73
|
-
| Per-resource deposit cap | optional | optional (primary control) | not applicable (capacity comes from the vault) |
|
|
74
|
-
|
|
|
71
|
+
| | **Core** | **Flux** | **FRV** | **Spoke** |
|
|
72
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------- |
|
|
73
|
+
| Underlying protocol | Venus Core lending | Fluid Lending | Venus Fixed-Rate Vaults | Venus isolated pools (hub-funded spoke) |
|
|
74
|
+
| Resource / receipt token | vToken (Compound-style) | fToken (ERC-4626 share) | FRV vault share (ERC-4626) | vToken (isolated-pools) |
|
|
75
|
+
| Deposit / withdraw call | `mint` / `redeemUnderlying` | `deposit` / `withdraw(assets)` | `deposit` / `withdraw(assets)` | `mint` / `redeemUnderlying` |
|
|
76
|
+
| Redeem-time pool fee | only if Comptroller `treasuryPercent` is later enabled (grossed-up); registration blocked while non-zero | none (Fluid's cut is in the supply rate) | none (reserve factor taken once at settlement) | none (the cut is the reserve factor, already inside the rate) |
|
|
77
|
+
| Spot APY source | `supplyRatePerBlock` × `blocksPerYear` | Fluid `LendingResolver` (pre-annualised APR) | vault `fixedAPY` (only while Fundraising / Lock) | `supplyRatePerBlock` × the **market's own** `blocksOrSecondsPerYear` |
|
|
78
|
+
| Per-resource deposit cap | optional | optional (primary control) | not applicable (capacity comes from the vault) | optional (per spoke pool, alongside the pool's own supply cap) |
|
|
79
|
+
| Who may supply | anyone | anyone | anyone | **allowlisted only** — the pool grants the YieldGroup address |
|
|
80
|
+
| Valuation basis | balance × exchange rate | `previewRedeem` | lifecycle-aware linear accrual | balance × exchange rate (includes `badDebt`) |
|
|
81
|
+
| Lifecycle constraint | none | none | 11-state machine (see below) | none |
|
|
75
82
|
|
|
76
83
|
## Deposit flow
|
|
77
84
|
|
|
@@ -191,10 +198,11 @@ Hub.totalAssets()
|
|
|
191
198
|
= Σ adapter.totalAssets(resource, yieldGroup) + YieldGroup idle balance
|
|
192
199
|
```
|
|
193
200
|
|
|
194
|
-
- **Fails closed.** `totalAssets()` is the share-price denominator, so a YieldGroup whose `totalAssets()` view reverts is **not** isolated here — it halts share math rather than silently dropping that YieldGroup's funds from NAV (which would understate the price, under-pay redeemers and over-credit depositors until the value reappeared). The _quantity
|
|
201
|
+
- **Fails closed.** `totalAssets()` is the share-price denominator, so a YieldGroup whose `totalAssets()` view reverts is **not** isolated here — it halts share math rather than silently dropping that YieldGroup's funds from NAV (which would understate the price, under-pay redeemers and over-credit depositors until the value reappeared). The _quantity views_ (`maxWithdraw`, and `HubLens`'s capacity figure) and the deposit/withdraw routing **do** isolate a faulting YieldGroup (contribute 0 / skip) because failing conservatively there is safe — only on the price would it corrupt the conversion. A view-level revert is a YieldGroup-level bug (resource-level reverts are isolated inside the YieldGroup, so external pauses surface in `withdraw`/`deposit`, not here); recover via the runbook below.
|
|
195
202
|
- Hub idle is normally zero (deposits route out atomically, withdrawals pull exact amounts, reallocate is balanced). A direct token donation persists and is intentionally counted into NAV (it accrues to LPs and is consumed first on withdraw) — the underlying cannot be swept.
|
|
196
203
|
- YieldGroup idle accumulates from treasury-fee gross-up surplus and dust handling on past withdrawals; it is counted and consumed idle-first.
|
|
197
204
|
- View paths use the **stored** exchange rate (`exchangeRateStored`, stale up to one accrual cycle, no state mutation). Mutating paths trigger interest accrual on the resource during mint/redeem, so the rate is current by the time the operation completes.
|
|
205
|
+
- **Spoke marks at the market's own exchange rate, and that rate includes written-off debt.** An isolated-pools `VToken` keeps written-off debt inside its rate: `healAccount` moves the shortfall from `totalBorrows` into `badDebt`, leaving the numerator unchanged, so the rate does not fall when a loss is booked. `AdapterSpokeV1` values a position at that same rate, as `AdapterCoreV1` does. The written-off amount is therefore cash the market does not have, not a lower share price, and while it is outstanding LPs who exit first are paid in full out of the cash that is left. The market's `shortfall` pays the debt down through `badDebtRecovered`, which lowers `badDebt` and raises cash by the same amount: more can be withdrawn and the share price does not move.
|
|
198
206
|
|
|
199
207
|
### Recovering a bricked YieldGroup (governance)
|
|
200
208
|
|
|
@@ -253,7 +261,60 @@ effectiveCap = (percentageCapBps == 10_000)
|
|
|
253
261
|
: min(absoluteCap, percentageCapBps × hubTVL / 10_000)
|
|
254
262
|
```
|
|
255
263
|
|
|
256
|
-
`percentageCapBps == 10_000` (100%) is a sentinel that **disables** the percentage component
|
|
264
|
+
`percentageCapBps == 10_000` (100%) is a sentinel that **disables** the percentage component, leaving only the absolute cap at every TVL. `absoluteCap == type(uint256).max` is rejected; use the sentinel to disable the percentage dimension instead.
|
|
265
|
+
|
|
266
|
+
**Enforcement is the routing cascade, not a quantity gate.** The Hub does not override `maxDeposit` / `maxMint`, so both inherit OpenZeppelin's bodies and return `type(uint256).max`. Solving the caps means solving every YieldGroup's dual cap against the TVL the deposit itself produces, and ERC-4626 calls those views on every `deposit` and every `mint` — that solve does not belong on the hot path. Instead `_routeDeposit` reverts with `HubCapacityExceeded(assets, placed)` on anything the cascade cannot place, which also names how much would have fit.
|
|
267
|
+
|
|
268
|
+
**The percentage cap is priced at the post-deposit TVL.** Incoming assets sit idle on the Hub for the whole cascade, so they are already inside `totalAssets()` when each `effectiveCap` is computed — a deposit lifts the very caps it is measured against. Capacity therefore cannot be priced at today's TVL. `HubLens.maxDeposit` solves for the largest TVL that every cap simultaneously admits and reports the distance to it: a book of Core `20,000 / 90%` and FRV `8,000 / 30%` tops out at `28,000`, and an empty Hub reports exactly that, not the `0` that pricing both caps at TVL = 0 produces. A frontend or integrator reads that figure before sizing a deposit; nothing on-chain requires it.
|
|
269
|
+
|
|
270
|
+
**Solving for the reachable TVL.** Let `X` be the post-deposit TVL. For each unpaused deposit-queue YieldGroup `i`, write `bal_i` for what it currently holds and
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
absBound_i = min( absoluteCap_i , bal_i + yieldGroup_i.maxDeposit() )
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
for the most it could ever hold — its absolute cap, further bounded by what its venue will accept on top of that balance. At TVL `X` it holds
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
hold_i(X) = max( bal_i , min( absBound_i , pct_i × X / 10_000 ) )
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The inner `min` is the dual cap. The outer `max` floors it at what the YieldGroup already holds, since a deposit cannot make it give money back. Writing `C` for the assets that are not free to move — Hub idle, paused YieldGroups, YieldGroups outside the deposit queue, and any whose capacity views revert — the reachable TVL is the largest `X` satisfying
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
X ≤ C + Σ hold_i(X)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
and `HubLens.maxDeposit` returns `X − totalAssets()`.
|
|
289
|
+
|
|
290
|
+
Each `hold_i` is flat, then sloped, then flat, with a breakpoint at each turn:
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
hold_i
|
|
294
|
+
^
|
|
295
|
+
| +----------- absBound_i absolute bound binds
|
|
296
|
+
| /
|
|
297
|
+
| / percentage cap binds, slope pct_i
|
|
298
|
+
| +------------+ bal_i balance floor binds
|
|
299
|
+
+--------+------------+--------------> TVL (X)
|
|
300
|
+
breakLow_i breakHigh_i
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`breakLow_i` is the TVL at which the percentage cap catches up with the balance, `breakHigh_i` the TVL at which it reaches `absBound_i`.
|
|
304
|
+
|
|
305
|
+
Summing over the queue gives at most `2n` breakpoints. Between two consecutive ones no YieldGroup changes which bound it sits on, so the sum is `base + slope × X / 10_000` there and the segment crosses in closed form:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
X = base × 10_000 / (10_000 − slope)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`HubCapacityMath` collects the breakpoints, sorts them, and keeps the largest crossing that lands inside its own segment. A segment whose `slope ≥ 10_000` has no crossing at all: its right-hand side grows at least as fast as `X`, so every TVL in it satisfies the inequality and the answer there is the segment's top edge, not a fixed point. That edge is snapped down to a multiple of `10_000`, where every percentage cap divides exactly and the cascade's per-YieldGroup flooring sheds nothing. Only a segment with no finite top edge, the last one, is passed over.
|
|
312
|
+
|
|
313
|
+
Membership is decided on the segment's own crossing, before any adjustment. Charging an adjustment first would push a crossing that lands exactly on a segment boundary just below it and discard the segment that holds the answer.
|
|
314
|
+
|
|
315
|
+
The crossing is then checked against the room the cascade would really leave at that TVL, and returned unchanged when it fits, which is the usual path. Only when it does not fit is the flooring pre-paid: the closed form floors once where the cascade floors each cap separately, so one wei is taken off `base` for each of the `k` YieldGroups the percentage cap binds on that segment. That pre-payment lowers the reported figure by `k × 10_000 / (10_000 − slope)` wei.
|
|
316
|
+
|
|
317
|
+
The figure promises only that depositing it succeeds, not that every smaller amount does. In a book with no absolute-only YieldGroup a dust deposit still reverts, because every `pctBps × TVL / 10_000` floors to zero while TVL is tiny.
|
|
257
318
|
|
|
258
319
|
**Resource level (YieldGroup) — optional, Core & Flux only.** Each resource may carry an optional absolute deposit cap limiting how much underlying _this YieldGroup_ holds in that one market, independent of the market's own protocol supply cap:
|
|
259
320
|
|
|
@@ -263,7 +324,7 @@ room = min( protocolHeadroom, resourceCap − ourBalanceInResource )
|
|
|
263
324
|
|
|
264
325
|
- `resourceCap == 0` means **unset/unbounded** — only the protocol cap binds. When unset, the balance lookup is skipped, so the feature is zero-cost when unused. (`raiseResourceCap` to 0 removes a cap; `lowerResourceCap` to 0 is rejected.)
|
|
265
326
|
- The protocol cap is **global/shared** across all depositors; the resource cap is purely about _this YieldGroup's_ position, so actual room can be below `resourceCap` if others have filled the market.
|
|
266
|
-
- Enforced everywhere deposits land: the inner deposit cascade, `maxDeposit()`, and resource-targeted reallocate pushes.
|
|
327
|
+
- Enforced everywhere deposits land: the inner deposit cascade, `YieldGroup.maxDeposit()`, and resource-targeted reallocate pushes.
|
|
267
328
|
- **YieldGroupFRV has no per-resource cap** — FRV capacity comes entirely from the vault's own `maxBorrowCap` and its Fundraising-only rule.
|
|
268
329
|
|
|
269
330
|
**Per-transaction withdrawal cap (Hub).** `maxWithdrawalSize` (asset units) bounds every single withdraw/redeem; exceeding it reverts `HubWithdrawCapExceeded`.
|
|
@@ -305,11 +366,12 @@ contracts/
|
|
|
305
366
|
core/ YieldGroupCore + Storage, Adapters/AdapterCoreV1
|
|
306
367
|
flux/ YieldGroupFlux + Storage, Adapters/AdapterFlux
|
|
307
368
|
FRV/ YieldGroupFRV + Storage, Adapters/AdapterFRV
|
|
369
|
+
lens/ HubLens.sol + HubCapacityMath.sol (off-chain read surface, no storage)
|
|
308
370
|
migrator/ Migrator.sol (immutable Core→Hub one-click migration)
|
|
309
371
|
registry/ HubRegistry.sol (chain-level Hub directory, discovery root)
|
|
310
|
-
interfaces/ IYieldGroupBase, IResourceAdapter, IHub, IHubRegistry, IYieldGroup*, IMigrator, IVToken, IVBep20, IWBNB, IFToken, IFRVVault, ...
|
|
311
|
-
deploy/ Foundry deploy scripts 01–
|
|
312
|
-
tests/foundry/ unit (Hub/, YieldGroup/{core,flux,frv}/, Registry/), Integration/, fork/
|
|
372
|
+
interfaces/ IYieldGroupBase, IResourceAdapter, IHub, IHubLens, IHubRegistry, IYieldGroup*, IMigrator, IVToken, IVBep20, IWBNB, IFToken, IFRVVault, ...
|
|
373
|
+
deploy/ Foundry deploy scripts 01–14 + DeployBase + config/<network>.json
|
|
374
|
+
tests/foundry/ unit (Hub/, YieldGroup/{core,flux,frv}/, Registry/, lens/), Integration/, fork/
|
|
313
375
|
deployments/<network>/ committed registry of deployed addresses (source of truth)
|
|
314
376
|
lib/ git-submodule dependencies
|
|
315
377
|
```
|
|
@@ -348,6 +410,16 @@ npm i @venusprotocol/liquidity-hub@develop # prerelease, cut from develop
|
|
|
348
410
|
import { IHub } from "@venusprotocol/liquidity-hub/contracts/interfaces/IHub.sol";
|
|
349
411
|
```
|
|
350
412
|
|
|
413
|
+
**Sizing a deposit.** `Hub.maxDeposit` / `Hub.maxMint` return `type(uint256).max` — the Hub does not compute a ceiling, because doing so would run the cap solve on every deposit. That holds while the Hub is paused too, so anything that sizes off them reverts on deposit. Read the real figure from `HubLens` instead:
|
|
414
|
+
|
|
415
|
+
```solidity
|
|
416
|
+
import { IHubLens } from "@venusprotocol/liquidity-hub/contracts/interfaces/IHubLens.sol";
|
|
417
|
+
|
|
418
|
+
uint256 room = IHubLens(lens).maxDeposit(hub); // 0 while the Hub is paused
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
`HubLens` is stateless and takes the Hub as a parameter, so one deployment serves every Hub on the chain; its address is in `deployments/<network>_addresses.json`. It also exposes `maxMint`, `depositCapacityBreakdown` (the same ceiling split by receiving YieldGroup) and `spotAPYBps` (the Hub's blended yield, weighted by balance, with idle counted at 0%). Depositing more than `maxDeposit` reverts `HubCapacityExceeded(assets, placed)`, which names how much would have fit.
|
|
422
|
+
|
|
351
423
|
**Addresses and ABIs.** `deployments/<network>_addresses.json` is a name → address map; each `deployments/<network>/<Name>.json` carries `address`, `block`, `chainId`, `contract` and the deployed `abi`.
|
|
352
424
|
|
|
353
425
|
```ts
|
|
@@ -357,13 +429,18 @@ import hub from "@venusprotocol/liquidity-hub/deployments/bscmainnet/Hub_USDT.js
|
|
|
357
429
|
|
|
358
430
|
## Deployment
|
|
359
431
|
|
|
360
|
-
Beacon-proxy model: one `UpgradeableBeacon` **per family per chain** (Hub, Core, FRV, Flux), each owned by governance — `beacon.upgradeTo` upgrades every vault of that family atomically. Per-asset instances are `BeaconProxy`s.
|
|
432
|
+
Beacon-proxy model: one `UpgradeableBeacon` **per family per chain** (Hub, Core, FRV, Flux, Spoke), each owned by governance — `beacon.upgradeTo` upgrades every vault of that family atomically. Per-asset instances are `BeaconProxy`s.
|
|
361
433
|
|
|
362
434
|
```
|
|
363
435
|
01_DeployHubBeacon 02_DeployCore 03_DeployFRV 04_DeployFlux # once per chain (impl + adapter + beacon)
|
|
364
436
|
05_DeployHubRegistry # once per chain (upgradeable singleton, discovery root)
|
|
365
437
|
06_DeployAssetVault # per asset, idempotent
|
|
366
438
|
07_DeployMigrator # once per chain (immutable, permissionless periphery)
|
|
439
|
+
10_DeployHubLens # once per chain, before 12 (stateless read surface; re-run to ship a new version)
|
|
440
|
+
11_DeployAdapters # upgrade only: new Core / Flux / FRV / Centrifuge / Spoke adapters
|
|
441
|
+
12_DeployHubImpl # upgrade only: new Hub impl behind the existing HubBeacon
|
|
442
|
+
13_DeploySpoke # once per chain (Spoke adapter + impl + beacon)
|
|
443
|
+
14_DeploySpokeSource # per asset, backfills existing Hubs
|
|
367
444
|
```
|
|
368
445
|
|
|
369
446
|
Scripts run in numeric order: beacons first, then the registry, then per-asset instances against both. Scripts only deploy + initialize — **ACM wiring, `addYieldGroup` / `addResource`, and queue configuration are governance (ACM-gated) actions done separately.**
|
|
@@ -374,6 +451,10 @@ Script 06 deploys a Hub proxy per asset (YieldGroups read `IHub.asset()` and rev
|
|
|
374
451
|
|
|
375
452
|
Script 07 deploys the immutable **Migrator** (no beacon, no proxy); it is permissionless and needs **no** ACM wiring — a user calls it directly after approving their vToken.
|
|
376
453
|
|
|
454
|
+
Scripts 13 and 14 add the **Spoke** family. They are split from 02–04/06 because script 06 skips any asset whose Hub already exists, so a Hub deployed before this family could never receive a Spoke source through it; 14 is keyed on the source instead and therefore backfills existing Hubs and covers new ones alike. The listing VIP must list the market and call `setAllowedSupplier(vToken, <SpokeSource>, true)` on each spoke pool **before** `addResource` — `AdapterSpokeV1.validateRegistration` rejects the registration otherwise, which is deliberate: a market that will not accept this source would otherwise occupy a queue slot it can never fill.
|
|
455
|
+
|
|
456
|
+
Scripts 11 and 12 ship new code to a chain whose families are already deployed, and are not part of onboarding. The family scripts do not fit that case: 01, 02 and 03 also deploy a new beacon and overwrite its record, 04 and 13 skip an adapter that is already recorded, and 08_DeployCentrifuge also deploys a YieldGroup implementation. Script 11 deploys only the five adapters; the YieldGroups keep the old adapter until governance calls `updateResourceAdapter(resource, adapter)` for each resource. Script 12 deploys only a Hub implementation and requires the `HubBeacon` record; the Hubs move to it when governance calls `HubBeacon.upgradeTo(impl)`. Both overwrite their registry records, so a dry run (no `--broadcast`) leaves `deployments/` modified; restore it with `git checkout deployments/`.
|
|
457
|
+
|
|
377
458
|
### Hub onboarding flow (deploy + one-tx registration VIP)
|
|
378
459
|
|
|
379
460
|
Onboarding an asset: the deployer EOA runs the scripts in numeric order (no ACM roles needed), then **one** governance proposal — a single hop — registers and wires the Hub atomically.
|
package/contracts/Hub/Hub.sol
CHANGED
|
@@ -26,6 +26,10 @@ import { HubAdminLib } from "./lib/HubAdminLib.sol";
|
|
|
26
26
|
* Inheritance order is load-bearing for upgrade safety — do not reorder.
|
|
27
27
|
* Function ordering follows the Solidity style guide: external → public → internal →
|
|
28
28
|
* private, mutating before view within each group.
|
|
29
|
+
* Capacity: `maxDeposit` / `maxMint` return `type(uint256).max`, paused or not, and do not
|
|
30
|
+
* reflect the caps. Size deposits and mints from `HubLens.maxDeposit` / `HubLens.maxMint`,
|
|
31
|
+
* which return the real figure and 0 while paused. The caps themselves are enforced by the
|
|
32
|
+
* deposit routing, which reverts `HubCapacityExceeded` on anything it cannot place.
|
|
29
33
|
* {HubAdminLib} is linked for EIP-170 headroom; a library declares no state, so it cannot shift this layout.
|
|
30
34
|
* @custom:oz-upgrades-unsafe-allow external-library-linking
|
|
31
35
|
*/
|
|
@@ -819,9 +823,9 @@ contract Hub is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeab
|
|
|
819
823
|
* donation. It is folded into NAV (the underlying cannot be swept) so it accrues to LPs and
|
|
820
824
|
* is consumed first on withdraw.
|
|
821
825
|
*
|
|
822
|
-
* Fails closed. A YieldGroup whose `totalAssets()` reverts is NOT isolated here — unlike
|
|
823
|
-
*
|
|
824
|
-
*
|
|
826
|
+
* Fails closed. A YieldGroup whose `totalAssets()` reverts is NOT isolated here — unlike
|
|
827
|
+
* `maxWithdraw` and the deposit / withdraw routing, which deliberately skip a faulting
|
|
828
|
+
* YieldGroup. The difference is the safety class: NAV is the
|
|
825
829
|
* share-price denominator that every mint and redeem converts against, so silently counting
|
|
826
830
|
* a YieldGroup as 0 would understate the price — under-paying redeemers and over-crediting
|
|
827
831
|
* depositors until its value reappeared. Failing to 0 is safe for a quantity gate (it only
|
|
@@ -850,16 +854,29 @@ contract Hub is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeab
|
|
|
850
854
|
total += IERC20Upgradeable(asset()).balanceOf(address(this));
|
|
851
855
|
}
|
|
852
856
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
857
|
+
// `maxDeposit` and `maxMint` are deliberately NOT overridden, so both inherit OpenZeppelin's
|
|
858
|
+
// bodies and return `type(uint256).max`, including while the Hub is paused (the override this
|
|
859
|
+
// replaced returned 0 then; deposits still revert, via `whenNotHubPaused`). Computing the real
|
|
860
|
+
// ceiling means solving every YieldGroup's dual cap against the TVL the deposit itself
|
|
861
|
+
// produces, and `deposit` / `mint` call these views on every single call — that solve does not
|
|
862
|
+
// belong on the hot path.
|
|
863
|
+
//
|
|
864
|
+
// Caps are still enforced, by `_routeDeposit`: whatever the cascade cannot place reverts with
|
|
865
|
+
// `HubCapacityExceeded(assets, placed)`, which also names how much would have fit. Restoring an
|
|
866
|
+
// override here would put the solve back on the hot path without tightening anything.
|
|
867
|
+
//
|
|
868
|
+
// The real figures live on `HubLens` (`maxDeposit`, `maxMint`, `depositCapacityBreakdown`),
|
|
869
|
+
// which is what a frontend or an integrator reads to size a deposit.
|
|
870
|
+
//
|
|
871
|
+
// What is inherited, kept here so the removal reads as deliberate rather than an oversight:
|
|
872
|
+
//
|
|
873
|
+
// function maxDeposit(address) public view override(ERC4626Upgradeable, IERC4626Upgradeable) returns (uint256) {
|
|
874
|
+
// return type(uint256).max;
|
|
875
|
+
// }
|
|
876
|
+
//
|
|
877
|
+
// function maxMint(address) public view override(ERC4626Upgradeable, IERC4626Upgradeable) returns (uint256) {
|
|
878
|
+
// return type(uint256).max;
|
|
879
|
+
// }
|
|
863
880
|
|
|
864
881
|
/**
|
|
865
882
|
* @inheritdoc IERC4626Upgradeable
|
|
@@ -1038,10 +1055,11 @@ contract Hub is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeab
|
|
|
1038
1055
|
// getting taxed as performance on the next recovery past it. The delta form shields the
|
|
1039
1056
|
// retained fee whether PPS sits above or below the mark, and stays monotonic (delta >= 0:
|
|
1040
1057
|
// the fee-on burn always destroys at least as much share-value as the assets routed out).
|
|
1041
|
-
// Genuine gains stay taxable: the increment
|
|
1042
|
-
// PPS
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1058
|
+
// Genuine gains stay taxable: the increment preserves the (hwm - pps) gap, so any yield
|
|
1059
|
+
// PPS earned on top of the uplift, including yield landed intra-block after the caller's
|
|
1060
|
+
// accrual, is still charged at the next accrual. `prePps` is taken before `_routeWithdraw`,
|
|
1061
|
+
// so a resource that redeems above its reported value has that surplus shielded too.
|
|
1062
|
+
// No-op when the exit fee is 0 (a proportional burn leaves PPS unchanged).
|
|
1045
1063
|
if (redeemBps != 0) {
|
|
1046
1064
|
uint256 ts = totalSupply();
|
|
1047
1065
|
if (ts != 0) {
|
|
@@ -1202,10 +1220,10 @@ contract Hub is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeab
|
|
|
1202
1220
|
* @dev When `percentageCapBps == BPS_DENOMINATOR` (100%), the percentage cap is treated
|
|
1203
1221
|
* as disabled and only the absolute cap binds. Without this carve-out, every Hub
|
|
1204
1222
|
* with any percentage-cap value is permanently un-bootstrappable: at TA = 0 the
|
|
1205
|
-
* percentage component collapses to 0,
|
|
1206
|
-
*
|
|
1223
|
+
* percentage component collapses to 0, so the cascade can place nothing and
|
|
1224
|
+
* `_routeDeposit` reverts on the very first deposit. The carve-out matches the
|
|
1207
1225
|
* MetaMorpho convention and preserves dual-cap semantics for `pctBps < 10_000`.
|
|
1208
|
-
* @dev Cap basis: every cap check (deposit cascade
|
|
1226
|
+
* @dev Cap basis: every cap check (deposit cascade and {reallocate}, plus `HubLens`) compares this
|
|
1209
1227
|
* ceiling against the YieldGroup's *current marked* `totalAssets()`, not the principal
|
|
1210
1228
|
* deposited into it. Because a group's `totalAssets()` accrues over time, its cap headroom
|
|
1211
1229
|
* shrinks even with no new deposit — most visibly for FRV, whose Hub-facing mark accrues the
|
|
@@ -1216,52 +1234,16 @@ contract Hub is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeab
|
|
|
1216
1234
|
* headroom for the coupon expected over the lock.
|
|
1217
1235
|
* @param cfg YieldGroup config to evaluate.
|
|
1218
1236
|
* @param ta TVL reference used for the percentage-cap component.
|
|
1219
|
-
* @return cap `pctBps == 10_000` ? `absoluteCap` : `min(absoluteCap, pctBps × ta / 10_000)`.
|
|
1237
|
+
* @return cap `pctBps == 10_000` ? `absoluteCap` : `min(absoluteCap, ⌈pctBps × ta / 10_000⌉)`.
|
|
1220
1238
|
*/
|
|
1221
1239
|
function _effectiveCap(IHub.YieldGroupConfig storage cfg, uint256 ta) internal view returns (uint256 cap) {
|
|
1222
1240
|
if (cfg.percentageCapBps == BPS_DENOMINATOR) return cfg.absoluteCap;
|
|
1223
|
-
|
|
1241
|
+
// Rounded up: percentage caps summing to 100% leave no slack, so a floored cap would leave
|
|
1242
|
+
// the room a wei short of the deposit that set it.
|
|
1243
|
+
uint256 pctCap = (ta * cfg.percentageCapBps + BPS_DENOMINATOR - 1) / BPS_DENOMINATOR;
|
|
1224
1244
|
cap = pctCap < cfg.absoluteCap ? pctCap : cfg.absoluteCap;
|
|
1225
1245
|
}
|
|
1226
1246
|
|
|
1227
|
-
/**
|
|
1228
|
-
* @notice Sum spare deposit capacity across the unpaused deposit-queue YieldGroups.
|
|
1229
|
-
* @param taSnapshot TVL reference used for percentage-cap math on each YieldGroup.
|
|
1230
|
-
* @return total Aggregate asset units that could be deposited right now.
|
|
1231
|
-
*/
|
|
1232
|
-
function _aggregateDepositCapacity(uint256 taSnapshot) internal view returns (uint256 total) {
|
|
1233
|
-
address[] storage queue = _outerDepositQueue;
|
|
1234
|
-
uint256 len = queue.length;
|
|
1235
|
-
for (uint256 i; i < len; ++i) {
|
|
1236
|
-
address yieldGroup = queue[i];
|
|
1237
|
-
IHub.YieldGroupConfig storage cfg = _yieldGroups[yieldGroup];
|
|
1238
|
-
if (cfg.registered && !cfg.paused) {
|
|
1239
|
-
// Isolate a misbehaving YieldGroup: a revert in either capacity view contributes 0
|
|
1240
|
-
// spare cap instead of bricking every deposit. Mirrors `totalAssets()`.
|
|
1241
|
-
uint256 currentBal;
|
|
1242
|
-
uint256 yieldGroupMax;
|
|
1243
|
-
bool ok = true;
|
|
1244
|
-
try IYieldGroupBase(yieldGroup).totalAssets() returns (uint256 v) {
|
|
1245
|
-
currentBal = v;
|
|
1246
|
-
} catch {
|
|
1247
|
-
ok = false;
|
|
1248
|
-
}
|
|
1249
|
-
if (ok) {
|
|
1250
|
-
try IYieldGroupBase(yieldGroup).maxDeposit() returns (uint256 v) {
|
|
1251
|
-
yieldGroupMax = v;
|
|
1252
|
-
} catch {
|
|
1253
|
-
ok = false;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
if (ok) {
|
|
1257
|
-
uint256 cap = _effectiveCap(cfg, taSnapshot);
|
|
1258
|
-
uint256 capRoom = cap > currentBal ? cap - currentBal : 0;
|
|
1259
|
-
total += capRoom < yieldGroupMax ? capRoom : yieldGroupMax;
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
1247
|
/**
|
|
1266
1248
|
* @notice Sum liquid funds across the unpaused withdraw-queue YieldGroups, plus Hub idle.
|
|
1267
1249
|
* @dev Hub idle is first-class liquidity: `_routeWithdraw` consumes it before touching any
|
|
@@ -1292,11 +1274,35 @@ contract Hub is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeab
|
|
|
1292
1274
|
|
|
1293
1275
|
// ============================== Private — mutating =======================
|
|
1294
1276
|
|
|
1277
|
+
/**
|
|
1278
|
+
* @notice Fail-open resource settlement used ONLY by the paused emergency path: a YieldGroup whose
|
|
1279
|
+
* `accrue()` reverts is skipped instead of bricking the whole call.
|
|
1280
|
+
* @dev Pairs with {_totalAssetsFailOpen}, the cap reference the same paused branch reads: a YieldGroup
|
|
1281
|
+
* that has stopped answering the Hub is what {emergencyReallocate} exists to unwind, so it must
|
|
1282
|
+
* not block the wind-down of the YieldGroups that still work. A skipped YieldGroup only leaves its
|
|
1283
|
+
* resources unsettled, and {_reallocatePullAll} still requires the delivered amount to equal the
|
|
1284
|
+
* leg, so an under-settled market reverts that leg. Share pricing keeps the strict
|
|
1285
|
+
* {_accrueResources} via {_accrueFees}.
|
|
1286
|
+
*/
|
|
1287
|
+
function _accrueResourcesFailOpen() private {
|
|
1288
|
+
address[] storage yieldGroups = _registeredYieldGroups;
|
|
1289
|
+
uint256 len = yieldGroups.length;
|
|
1290
|
+
for (uint256 i; i < len; ++i) {
|
|
1291
|
+
// solhint-disable-next-line no-empty-blocks
|
|
1292
|
+
try IYieldGroupBase(yieldGroups[i]).accrue() { }
|
|
1293
|
+
catch {
|
|
1294
|
+
emit YieldGroupAccrualFailed(yieldGroups[i]);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1295
1299
|
/**
|
|
1296
1300
|
* @notice Shared net-zero reallocation core for {reallocate} and {emergencyReallocate}.
|
|
1297
|
-
* @dev Accrues fees first so the operation trades against current state
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1301
|
+
* @dev Accrues fees first so the operation trades against current state. A pause skips only the
|
|
1302
|
+
* FEE half, so the frozen window stays out of fee proration ({unpauseHub} later advances
|
|
1303
|
+
* the cursor past it). Resource settlement still runs — adapters size withdrawals off
|
|
1304
|
+
* stored resource state — and charges nothing, moving neither the HWM nor the cursor. Under
|
|
1305
|
+
* the freeze that settlement is fail-open, matching the NAV read it feeds.
|
|
1300
1306
|
* Enforces `Σ withdraws == Σ deposits`.
|
|
1301
1307
|
* @param withdraws Withdraw legs to execute first.
|
|
1302
1308
|
* @param deposits Deposit legs to execute against the post-pull TVL snapshot.
|
|
@@ -1304,6 +1310,8 @@ contract Hub is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeab
|
|
|
1304
1310
|
function _reallocate(IHub.ReallocateLeg[] calldata withdraws, IHub.ReallocateLeg[] calldata deposits) private {
|
|
1305
1311
|
if (!_hubPaused) {
|
|
1306
1312
|
_accrueFees();
|
|
1313
|
+
} else {
|
|
1314
|
+
_accrueResourcesFailOpen();
|
|
1307
1315
|
}
|
|
1308
1316
|
|
|
1309
1317
|
uint256 withdrawSum = _reallocatePullAll(withdraws);
|
|
@@ -290,6 +290,11 @@ contract AdapterCentrifuge is IAdapterCentrifuge {
|
|
|
290
290
|
+ inv.claimableCancelRedeemRequest;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/// @inheritdoc IResourceAdapter
|
|
294
|
+
function resourceName(address resource) external view override returns (string memory) {
|
|
295
|
+
return IERC20Metadata(ICentrifugeAsyncVault(resource).share()).name();
|
|
296
|
+
}
|
|
297
|
+
|
|
293
298
|
/// @inheritdoc IResourceAdapter
|
|
294
299
|
/// @dev Verifies the vault exposes the share token, request manager, investment state, and price
|
|
295
300
|
/// reads required by this adapter.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity 0.8.25;
|
|
3
3
|
|
|
4
4
|
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
5
6
|
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
6
7
|
|
|
7
8
|
import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
|
|
@@ -248,6 +249,11 @@ contract AdapterCoreV1 is IResourceAdapter {
|
|
|
248
249
|
return IVToken(resource).balanceOf(holder);
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
/// @inheritdoc IResourceAdapter
|
|
253
|
+
function resourceName(address resource) external view override returns (string memory) {
|
|
254
|
+
return IERC20Metadata(resource).name();
|
|
255
|
+
}
|
|
256
|
+
|
|
251
257
|
/// @inheritdoc IResourceAdapter
|
|
252
258
|
/// @dev Rejects a market whose Comptroller charges a non-zero `treasuryPercent`: that exit fee
|
|
253
259
|
/// is unmodeled in the Hub's gross NAV (see the treasury-fee note above). The withdraw
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity 0.8.25;
|
|
3
3
|
|
|
4
4
|
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
5
6
|
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
6
7
|
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
|
|
7
8
|
|
|
@@ -148,12 +149,17 @@ contract AdapterFRV is IResourceAdapter {
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/// @inheritdoc IResourceAdapter
|
|
151
|
-
/// @dev Returns the vault's `fixedAPY` (already annualised in BPS)
|
|
152
|
-
/// actually earning — `Fundraising` or `Lock`. In any other state the
|
|
153
|
-
/// nothing (matured/idle awaiting redemption, failed, liquidated, or
|
|
154
|
-
/// so report 0 rather than the contractual rate; otherwise dead capital
|
|
155
|
-
/// the YieldGroup's TVL-weighted spot APY. The `blocksPerYear` parameter is
|
|
156
|
-
/// yields are time-based, not block-based.
|
|
152
|
+
/// @dev Returns the vault's `fixedAPY` (already annualised in BPS) net of `reserveFactor`, only
|
|
153
|
+
/// while the vault is actually earning — `Fundraising` or `Lock`. In any other state the
|
|
154
|
+
/// capital earns nothing (matured/idle awaiting redemption, failed, liquidated, or
|
|
155
|
+
/// pre-fundraising), so report 0 rather than the contractual rate; otherwise dead capital
|
|
156
|
+
/// would inflate the YieldGroup's TVL-weighted spot APY. The `blocksPerYear` parameter is
|
|
157
|
+
/// unused since FRV yields are time-based, not block-based.
|
|
158
|
+
///
|
|
159
|
+
/// `fixedAPY` is the rate the institution pays; the vault keeps `reserveFactor` of the
|
|
160
|
+
/// interest for the protocol at settlement, so a supplier earns the remainder. This view is
|
|
161
|
+
/// defined as the supply-side rate, so it reports that remainder, netted the same way
|
|
162
|
+
/// {_accruedTotalAssets} nets the value it marks: floor the cut, then subtract it.
|
|
157
163
|
function spotAPYBps(
|
|
158
164
|
address resource,
|
|
159
165
|
uint256 /* blocksPerYear */
|
|
@@ -168,8 +174,17 @@ contract AdapterFRV is IResourceAdapter {
|
|
|
168
174
|
return 0;
|
|
169
175
|
}
|
|
170
176
|
IFRVVault.FRVVaultConfig memory cfg = IFRVVault(resource).config();
|
|
177
|
+
|
|
178
|
+
// Saturate before netting, not after: a misreporting vault can carry a `fixedAPY` large
|
|
179
|
+
// enough that `fixedAPY * reserveFactor` overflows, and this view must not revert. The
|
|
180
|
+
// YieldGroup sums it across every resource, so one bad vault would take the whole reading
|
|
181
|
+
// down. Below `uint64.max` the clamp is a no-op and the arithmetic is exact.
|
|
182
|
+
uint256 grossApy = cfg.fixedAPY > type(uint64).max ? type(uint64).max : cfg.fixedAPY;
|
|
183
|
+
|
|
184
|
+
// `reserveFactor <= MANTISSA_ONE` is enforced by the FRV vault controller at config time, so
|
|
185
|
+
// the cut never exceeds `grossApy` and this subtraction cannot underflow.
|
|
171
186
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
172
|
-
return
|
|
187
|
+
return uint64(grossApy - (grossApy * cfg.reserveFactor) / MANTISSA_ONE);
|
|
173
188
|
}
|
|
174
189
|
|
|
175
190
|
/// @inheritdoc IResourceAdapter
|
|
@@ -177,6 +192,11 @@ contract AdapterFRV is IResourceAdapter {
|
|
|
177
192
|
return IFRVVault(resource).balanceOf(holder);
|
|
178
193
|
}
|
|
179
194
|
|
|
195
|
+
/// @inheritdoc IResourceAdapter
|
|
196
|
+
function resourceName(address resource) external view override returns (string memory) {
|
|
197
|
+
return IERC20Metadata(resource).name();
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
/// @inheritdoc IResourceAdapter
|
|
181
201
|
/// @dev No-op: FRV takes its `reserveFactor` cut once at settlement, not per-redeem, so there
|
|
182
202
|
/// is no unmodeled exit fee to guard against at registration.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity 0.8.25;
|
|
3
3
|
|
|
4
4
|
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
5
6
|
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
6
7
|
|
|
7
8
|
import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
|
|
@@ -162,6 +163,11 @@ contract AdapterFlux is IResourceAdapter {
|
|
|
162
163
|
return IFToken(resource).balanceOf(holder);
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
/// @inheritdoc IResourceAdapter
|
|
167
|
+
function resourceName(address resource) external view override returns (string memory) {
|
|
168
|
+
return IERC20Metadata(resource).name();
|
|
169
|
+
}
|
|
170
|
+
|
|
165
171
|
/// @inheritdoc IResourceAdapter
|
|
166
172
|
/// @dev No-op: a Fluid fToken takes its revenue cut in the supply rate, not per-redeem, so
|
|
167
173
|
/// there is no unmodeled exit fee to guard against at registration.
|