@venusprotocol/liquidity-hub 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +422 -0
- package/contracts/Hub/Hub.sol +1614 -0
- package/contracts/Hub/HubStorage.sol +106 -0
- package/contracts/Hub/lib/HubAdminLib.sol +292 -0
- package/contracts/YieldGroup/YieldGroup.sol +128 -0
- package/contracts/YieldGroup/YieldGroupFRV.sol +107 -0
- package/contracts/YieldGroup/base/YieldGroupBase.sol +797 -0
- package/contracts/YieldGroup/base/YieldGroupBaseStorage.sol +73 -0
- package/contracts/adapters/AdapterCoreV1.sol +309 -0
- package/contracts/adapters/AdapterFRV.sol +276 -0
- package/contracts/adapters/AdapterFlux.sol +173 -0
- package/contracts/interfaces/IHub.sol +551 -0
- package/contracts/interfaces/IHubRegistry.sol +120 -0
- package/contracts/interfaces/IMigrator.sol +134 -0
- package/contracts/interfaces/IResourceAdapter.sol +154 -0
- package/contracts/interfaces/IYieldGroup.sol +119 -0
- package/contracts/interfaces/IYieldGroupBase.sol +507 -0
- package/contracts/interfaces/IYieldGroupFRV.sol +61 -0
- package/contracts/interfaces/external/IComptroller.sol +57 -0
- package/contracts/interfaces/external/IFRVVault.sol +113 -0
- package/contracts/interfaces/external/IFToken.sol +46 -0
- package/contracts/interfaces/external/IFluidLendingResolver.sol +63 -0
- package/contracts/interfaces/external/IVBep20.sol +39 -0
- package/contracts/interfaces/external/IVToken.sol +125 -0
- package/contracts/interfaces/external/IWBNB.sol +16 -0
- package/contracts/migrator/Migrator.sol +182 -0
- package/contracts/registry/HubRegistry.sol +179 -0
- package/deployments/bscmainnet/AdapterCoreV1.json +336 -0
- package/deployments/bscmainnet/AdapterFRV.json +225 -0
- package/deployments/bscmainnet/AdapterFlux.json +249 -0
- package/deployments/bscmainnet/CoreBeacon.json +110 -0
- package/deployments/bscmainnet/CoreSource_U.json +1083 -0
- package/deployments/bscmainnet/CoreSource_USDC.json +1083 -0
- package/deployments/bscmainnet/CoreSource_USDT.json +1083 -0
- package/deployments/bscmainnet/FRVBeacon.json +110 -0
- package/deployments/bscmainnet/FRVSource_U.json +978 -0
- package/deployments/bscmainnet/FRVSource_USDC.json +978 -0
- package/deployments/bscmainnet/FRVSource_USDT.json +978 -0
- package/deployments/bscmainnet/FluxBeacon.json +110 -0
- package/deployments/bscmainnet/FluxSource_U.json +1083 -0
- package/deployments/bscmainnet/FluxSource_USDC.json +1083 -0
- package/deployments/bscmainnet/FluxSource_USDT.json +1083 -0
- package/deployments/bscmainnet/HubBeacon.json +110 -0
- package/deployments/bscmainnet/HubImpl.json +2216 -0
- package/deployments/bscmainnet/HubRegistry.json +386 -0
- package/deployments/bscmainnet/HubRegistryImpl.json +386 -0
- package/deployments/bscmainnet/Hub_U.json +2216 -0
- package/deployments/bscmainnet/Hub_USDC.json +2216 -0
- package/deployments/bscmainnet/Hub_USDT.json +2216 -0
- package/deployments/bscmainnet/Migrator.json +375 -0
- package/deployments/bscmainnet/YieldGroupCoreImpl.json +1083 -0
- package/deployments/bscmainnet/YieldGroupFRVImpl.json +978 -0
- package/deployments/bscmainnet/YieldGroupFluxImpl.json +1083 -0
- package/deployments/bscmainnet_addresses.json +32 -0
- package/deployments/bsctestnet/AdapterCoreV1.json +336 -0
- package/deployments/bsctestnet/AdapterFRV.json +225 -0
- package/deployments/bsctestnet/AdapterFlux.json +249 -0
- package/deployments/bsctestnet/CoreBeacon.json +110 -0
- package/deployments/bsctestnet/CoreSource_USDT.json +1083 -0
- package/deployments/bsctestnet/FRVBeacon.json +110 -0
- package/deployments/bsctestnet/FRVSource_USDT.json +978 -0
- package/deployments/bsctestnet/FluxBeacon.json +110 -0
- package/deployments/bsctestnet/FluxSource_USDT.json +1083 -0
- package/deployments/bsctestnet/HubBeacon.json +110 -0
- package/deployments/bsctestnet/HubImpl.json +2216 -0
- package/deployments/bsctestnet/HubRegistry.json +386 -0
- package/deployments/bsctestnet/HubRegistryImpl.json +386 -0
- package/deployments/bsctestnet/HubRegistryProxyAdmin.json +157 -0
- package/deployments/bsctestnet/Hub_USDT.json +2216 -0
- package/deployments/bsctestnet/Migrator.json +375 -0
- package/deployments/bsctestnet/YieldGroupCoreImpl.json +1083 -0
- package/deployments/bsctestnet/YieldGroupFRVImpl.json +978 -0
- package/deployments/bsctestnet/YieldGroupFluxImpl.json +1083 -0
- package/deployments/bsctestnet_addresses.json +25 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
4
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
5
|
+
|
|
6
|
+
-----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
|
|
10
|
+
Licensor: Venus Protocol
|
|
11
|
+
|
|
12
|
+
Licensed Work: Venus Liquidity Hub
|
|
13
|
+
The Licensed Work is (c) 2026 Venus Protocol
|
|
14
|
+
|
|
15
|
+
Additional Use Grant: None
|
|
16
|
+
|
|
17
|
+
Change Date: 2030-08-12
|
|
18
|
+
|
|
19
|
+
Change License: GNU General Public License v2.0 or later
|
|
20
|
+
|
|
21
|
+
-----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
Terms
|
|
24
|
+
|
|
25
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
26
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
27
|
+
Licensor may make an Additional Use Grant, above, permitting limited
|
|
28
|
+
production use.
|
|
29
|
+
|
|
30
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
31
|
+
available distribution of a specific version of the Licensed Work under this
|
|
32
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
33
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
34
|
+
above terminate.
|
|
35
|
+
|
|
36
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
37
|
+
currently in effect as described in this License, you must purchase a
|
|
38
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
39
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
40
|
+
|
|
41
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
42
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
43
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
44
|
+
for each version of the Licensed Work released by Licensor.
|
|
45
|
+
|
|
46
|
+
You must conspicuously display this License on each original or modified copy
|
|
47
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
48
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
49
|
+
License apply to your use of that work.
|
|
50
|
+
|
|
51
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
52
|
+
terminate your rights under this License for the current and all other
|
|
53
|
+
versions of the Licensed Work.
|
|
54
|
+
|
|
55
|
+
This License does not grant you any right in any trademark or logo of
|
|
56
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
57
|
+
Licensor as expressly required by this License).
|
|
58
|
+
|
|
59
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
60
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
61
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
62
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
63
|
+
TITLE.
|
|
64
|
+
|
|
65
|
+
MariaDB hereby grants you permission to use this License’s text to license
|
|
66
|
+
your works, and to refer to it using the trademark "Business Source License",
|
|
67
|
+
as long as you comply with the Covenants of Licensor below.
|
|
68
|
+
|
|
69
|
+
-----------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
Covenants of Licensor
|
|
72
|
+
|
|
73
|
+
In consideration of the right to use this License’s text and the "Business
|
|
74
|
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
|
75
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
76
|
+
|
|
77
|
+
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
|
78
|
+
or a license that is compatible with GPL Version 2.0 or a later version,
|
|
79
|
+
where "compatible" means that software provided under the Change License can
|
|
80
|
+
be included in a program with software provided under GPL Version 2.0 or a
|
|
81
|
+
later version. Licensor may specify additional Change Licenses without
|
|
82
|
+
limitation.
|
|
83
|
+
|
|
84
|
+
2. To either: (a) specify an additional grant of rights to use that does not
|
|
85
|
+
impose any additional restriction on the right granted in this License, as
|
|
86
|
+
the Additional Use Grant; or (b) insert the text "None".
|
|
87
|
+
|
|
88
|
+
3. To specify a Change Date.
|
|
89
|
+
|
|
90
|
+
4. Not to modify this License in any other way.
|
|
91
|
+
|
|
92
|
+
-----------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
Notice
|
|
95
|
+
|
|
96
|
+
The Business Source License (this document, or the "License") is not an Open
|
|
97
|
+
Source license. However, the Licensed Work will eventually be made available
|
|
98
|
+
under an Open Source License, as stated in this License.
|
package/README.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Venus Liquidity Hub
|
|
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.
|
|
4
|
+
|
|
5
|
+
> **Scope.** v1 ships on BNB Chain with three yield families — **Core**, **Flux**, **FRV**. Isolated Pools and RWA (Ondo / BUIDL) are documented future YieldGroups, not v1. The fee machinery is fully implemented but launches at `0/0`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
Three-tier routing: the Hub routes across per-asset YieldGroups, each YieldGroup owns an inner queue of one or more resources, and a stateless adapter speaks the protocol-specific ABI for each resource. The Hub depends only on the `IYieldGroupBase` interface and never reaches past a YieldGroup into the underlying market.
|
|
12
|
+
|
|
13
|
+
**Hub > YieldGroup > Adapter (delegatecall) > Resource**
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart TB
|
|
17
|
+
classDef hub fill:#e8f5e9,stroke:#2e7d32,color:#000,font-weight:bold
|
|
18
|
+
classDef src fill:#fff3e0,stroke:#ef6c00,color:#000,font-weight:bold
|
|
19
|
+
classDef queue fill:#e3f2fd,stroke:#1565c0,color:#000
|
|
20
|
+
classDef adapter fill:#fce4ec,stroke:#c2185b,color:#000
|
|
21
|
+
classDef resource fill:#e1f5fe,stroke:#0277bd,color:#000
|
|
22
|
+
|
|
23
|
+
User((User))
|
|
24
|
+
User -->|"deposit / withdraw"| Hub
|
|
25
|
+
|
|
26
|
+
Hub["Hub_USDT (ERC-4626, beacon proxy)<br/>dual caps · fees · per-tx cap"]:::hub
|
|
27
|
+
|
|
28
|
+
Hub --> OuterQ["outer queue (per direction)<br/>[YieldGroup_Core, YieldGroup_Flux, YieldGroup_FRV]"]:::queue
|
|
29
|
+
|
|
30
|
+
OuterQ --> CoreYG["YieldGroup_Core_USDT"]:::src
|
|
31
|
+
OuterQ --> FluxYG["YieldGroup_Flux_USDT"]:::src
|
|
32
|
+
OuterQ --> FrvYG["YieldGroup_FRV_USDT"]:::src
|
|
33
|
+
|
|
34
|
+
CoreYG --> CoreInner["inner queue [vUSDT_v1, ...]"]:::queue
|
|
35
|
+
FluxYG --> FluxInner["inner queue [fUSDT, ...]"]:::queue
|
|
36
|
+
FrvYG --> FrvInner["inner queue [FRV_A, FRV_B, ...]"]:::queue
|
|
37
|
+
|
|
38
|
+
CoreInner -->|"delegatecall"| ACore["AdapterCoreV1<br/>(stateless singleton)"]:::adapter
|
|
39
|
+
FluxInner -->|"delegatecall"| AFlux["AdapterFlux<br/>(stateless singleton)"]:::adapter
|
|
40
|
+
FrvInner -->|"delegatecall"| AFrv["AdapterFRV<br/>(stateless singleton)"]:::adapter
|
|
41
|
+
|
|
42
|
+
ACore --> RCore["vUSDT (Venus Core vToken)"]:::resource
|
|
43
|
+
AFlux --> RFlux["fUSDT (Fluid fToken)"]:::resource
|
|
44
|
+
AFrv --> RFrv["FRV vault (fixed-rate, lifecycle)"]:::resource
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Key contracts
|
|
48
|
+
|
|
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
|
+
| **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`). |
|
|
56
|
+
| **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. |
|
|
57
|
+
| **IYieldGroupBase** | Hub↔YieldGroup boundary. `deposit` / `withdraw` (queue) + `depositResource` / `withdrawResource` (targeted, reallocate-only) + `totalAssets` / `maxDeposit` / `maxWithdraw` / `spotAPYBps`. |
|
|
58
|
+
| **IResourceAdapter** | YieldGroup↔adapter boundary. Mutating fns delegatecalled; view fns take an explicit `holder`. `validateRegistration` rejects resources with an unmodeled per-redeem fee. |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
> **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
|
+
|
|
64
|
+
## The three yield families
|
|
65
|
+
|
|
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
|
+
| Lifecycle constraint | none | none | 11-state machine (see below) |
|
|
75
|
+
|
|
76
|
+
## Deposit flow
|
|
77
|
+
|
|
78
|
+
```mermaid
|
|
79
|
+
flowchart LR
|
|
80
|
+
classDef default fill:#f9f9f9,stroke:#333,color:#000
|
|
81
|
+
|
|
82
|
+
U((User)) -->|"assets"| H[Hub]
|
|
83
|
+
H -->|"outer deposit queue"| YG[YieldGroup]
|
|
84
|
+
YG -->|"inner deposit queue<br/>delegatecall"| A[Adapter]
|
|
85
|
+
A -->|"mint / deposit"| M["Resource<br/>(vToken / fToken / FRV)"]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
1. User calls `Hub.deposit(assets, receiver)` (or `mint`).
|
|
89
|
+
2. Hub accrues fees, then pulls `assets` from the user — so the caller trades against the post-accrual share price.
|
|
90
|
+
3. Hub snapshots `totalAssets()` (already includes the just-transferred idle) and walks the **outer deposit queue**.
|
|
91
|
+
4. For each YieldGroup: skip if unregistered or paused; compute `capRoom = max(0, effectiveCap − yieldGroup.totalAssets())`; send `min(remaining, capRoom, yieldGroup.maxDeposit())`; revert `YieldGroupUnderfilled` if the YieldGroup places less.
|
|
92
|
+
5. Each YieldGroup walks its **inner deposit queue**, delegatecalling its adapter per resource (respecting any per-resource cap), and mints receipt tokens into the YieldGroup.
|
|
93
|
+
6. Hub mints share tokens to the receiver.
|
|
94
|
+
7. If capacity across the whole queue is short → **the entire tx reverts** (`HubCapacityExceeded`). No partial fill.
|
|
95
|
+
|
|
96
|
+
## Withdrawal flow
|
|
97
|
+
|
|
98
|
+
```mermaid
|
|
99
|
+
flowchart RL
|
|
100
|
+
classDef default fill:#f9f9f9,stroke:#333,color:#000
|
|
101
|
+
|
|
102
|
+
M["Resource<br/>(vToken / fToken / FRV)"] -->|"redeem"| A[Adapter]
|
|
103
|
+
A -->|"delegatecall"| YG[YieldGroup]
|
|
104
|
+
YG -->|"outer withdraw queue"| H[Hub]
|
|
105
|
+
H -->|"assets"| U((User))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
1. User calls `Hub.withdraw(assets, receiver, owner)` (or `redeem`).
|
|
109
|
+
2. Hub enforces the **per-tx withdrawal cap** (`HubWithdrawCapExceeded`), accrues fees, then burns shares from `owner`.
|
|
110
|
+
3. Hub consumes its own **idle balance first**, then walks the **outer withdraw queue** (independent of the deposit queue).
|
|
111
|
+
4. Hub calls `YieldGroup.withdraw(amount, hub)`; the YieldGroup uses its idle balance first, then walks its **inner withdraw queue**, delegatecalling the adapter's `redeemUnderlying` / `withdraw`.
|
|
112
|
+
5. Hub measures the delivered balance delta (rather than trusting a return value) and reverts `YieldGroupUnderfilled` on any shortfall.
|
|
113
|
+
6. Underlying flows Resource → YieldGroup → Hub → receiver.
|
|
114
|
+
7. If liquidity is short → **the entire tx reverts** (`HubInsufficientLiquidity`). No partial fill.
|
|
115
|
+
|
|
116
|
+
### Operator invariant: deposit queue ⊆ withdraw queue
|
|
117
|
+
|
|
118
|
+
Keep every deposit-queue member (outer and inner) in the matching withdraw queue. Withdrawals walk
|
|
119
|
+
only the withdraw queues, so a target that is funded but off the withdraw queue holds value that
|
|
120
|
+
counts in `totalAssets()` yet is not reachable by the normal redeem path. This is an operator
|
|
121
|
+
responsibility, not an on-chain constraint. It is reached most often by ordinary deposits, not just
|
|
122
|
+
`reallocate`, and it is fully recoverable in one tx with no loss:
|
|
123
|
+
|
|
124
|
+
- **(a)** re-set the withdraw queue to include the target, or **(b)** drain it via `reallocate`
|
|
125
|
+
(a pull leg accepts any registered target, on-queue or not).
|
|
126
|
+
|
|
127
|
+
`maxWithdraw()` already excludes off-queue value, so it stays the honest redeemable figure throughout.
|
|
128
|
+
|
|
129
|
+
## Reallocate flow
|
|
130
|
+
|
|
131
|
+
```mermaid
|
|
132
|
+
flowchart LR
|
|
133
|
+
classDef default fill:#f9f9f9,stroke:#333,color:#000
|
|
134
|
+
classDef op fill:#ffebee,stroke:#c62828,color:#000
|
|
135
|
+
|
|
136
|
+
Op((Operator)):::op -->|"reallocate"| H[Hub]
|
|
137
|
+
H -->|"pull (all legs)"| SrcA["YieldGroup A<br/>(withdraw legs)"]
|
|
138
|
+
H -->|"push (all legs)"| SrcB["YieldGroup B<br/>(deposit legs)"]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Reallocate moves assets between YieldGroups — and, optionally, between specific resources within a YieldGroup — without funds entering or leaving the Hub. Both legs share one struct:
|
|
142
|
+
|
|
143
|
+
```solidity
|
|
144
|
+
struct ReallocateLeg { address yieldGroup; address resource; uint256 amount; }
|
|
145
|
+
// resource == address(0) → route through the YieldGroup's inner queue (queue-decided)
|
|
146
|
+
// resource != address(0) → target that exact market/vault, bypassing the inner queue
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The Hub treats `resource` as an **opaque pass-through**: it holds no resource registry and never reads resource state — it relays the address to the owning YieldGroup, which validates it against its own registry. A targeted leg moves the full `amount` into/out of that one resource or reverts (no partial fill). Setting both legs to the same `yieldGroup` performs an **intra-YieldGroup move** (e.g. vUSDT_v1 → vUSDT_v2).
|
|
150
|
+
|
|
151
|
+
1. Operator calls `Hub.reallocate(withdraws[], deposits[])`.
|
|
152
|
+
2. Hub accrues fees (skipped while paused).
|
|
153
|
+
3. **Pull phase**: every withdraw leg runs first — `YieldGroup.withdraw(amount, hub)` (queue) or `YieldGroup.withdrawResource(resource, amount, hub)` (targeted). Underlying returns to the Hub as idle. Pulling from a _paused_ resource is allowed (wind-down).
|
|
154
|
+
4. Hub takes a single `totalAssets()` snapshot after all pulls (the cap reference for every push — valid because a balanced reallocate conserves TVL).
|
|
155
|
+
5. **Push phase**: each deposit leg checks the YieldGroup is registered, unpaused, and within its effective cap, then `YieldGroup.deposit(amount)` (queue) or `YieldGroup.depositResource(resource, amount)` (targeted). Depositing into a paused resource reverts.
|
|
156
|
+
6. Hub enforces `Σ withdraws == Σ deposits` (`ReallocateImbalanced` otherwise). **Net-zero invariant.**
|
|
157
|
+
7. The Operator can only move funds among already-registered YieldGroups/resources — it can never create new routes.
|
|
158
|
+
|
|
159
|
+
> **`emergencyReallocate`** is a separate, governance-gated function carrying its own ACM role. It does the same net-zero rebalance but **remains callable while the Hub is paused**, giving governance a wind-down lever without granting the Operator a pause bypass. It is a routing / containment tool, not an unlock: each pull leg is still bound by the resource's withdrawability, so it cannot extract FRV principal from a locked vault (`maxWithdraw` is 0 until the vault reaches a terminal state). It routes around FRV exposure and moves liquid resources; locked FRV principal is only reachable once that vault is Matured / Failed / Liquidated.
|
|
160
|
+
|
|
161
|
+
**Example A — coarse move, 100k Core → FRV (`resource = 0`):** the YieldGroup inner queues decide which markets are touched. Core walks its withdraw queue (idle-first, then v1, v2…); FRV walks its deposit queue, overflowing from one vault to the next as caps fill.
|
|
162
|
+
|
|
163
|
+
**Example B — targeted intra-YieldGroup move, 100k vUSDT_v1 → vUSDT_v2:** redeems exactly 100k from v1 (bypassing the queue and idle-first), then deposits exactly 100k into v2 or reverts if v2 lacks capacity or is paused.
|
|
164
|
+
|
|
165
|
+
## FRV lifecycle
|
|
166
|
+
|
|
167
|
+
FRV vaults are ERC-4626 with an **11-state machine** on top. The YieldGroup calls the vault's permissionless `updateVaultState()` _before every_ capacity/liquidity check so routing always sees the current state — never a stale one.
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
WaitingForMargin → MarginDeposited → Fundraising → InstitutionConfirmation
|
|
171
|
+
→ Lock → PendingSettlement → Matured (happy path)
|
|
172
|
+
↘ SettlementDeadlineExceeded / Failed / Liquidated → Closed
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- **Deposits** are accepted **only in `Fundraising`** (`maxDeposit` is 0 in every other state). Each vault has a `minSupplierDeposit` floor: a sub-floor cascade leg is skipped to the next vault, a sub-floor targeted `depositResource` reverts cleanly — but a sub-floor amount that exactly fills the vault's remaining capacity (the residual tail) is always accepted.
|
|
176
|
+
- **Withdrawals** are possible **only in the terminal states** `Matured`, `Failed`, or `Liquidated` — capital is locked through `Lock` and `PendingSettlement`. `Matured` adds the fixed-rate yield; `Failed` / `Liquidated` can return **less than principal**, which marks down both `maxWithdraw` and `totalAssets`.
|
|
177
|
+
- **Valuation accrues linearly — no maturity cliff.** While the vault sits in `Lock` the position is marked at `principal + netInterest × elapsed / lockDuration` — a smooth rise rather than a step at settlement, so there is no reprice for a sandwicher to capture. The mark accrues on lock state alone, independent of whether the institution has drawn the principal: the vault stamps `lockStartTime` at fundraising close and owes interest over the whole lock window regardless of the draw, so gating on the draw would instead introduce a jump in the single transaction that flips it. `netInterest` is full-term interest minus the floored reserve cut (mirroring the vault's own settlement math), so the `Lock`-end mark equals the full-repayment `settlementAmount` exactly (continuity, not a discontinuity). `PendingSettlement` / `SettlementDeadlineExceeded` hold flat at `principal + full netInterest`; terminal states price directly off `previewRedeem`.
|
|
178
|
+
- **A defaulting loan is marked above realizable value until it settles.** Holding the full `principal + netInterest` mark through `PendingSettlement` / `SettlementDeadlineExceeded` means a loan heading for a shortfall reads high until the terminal step-down. The over-mark is bounded by the Hub's per-YieldGroup cap (it limits how large the position can grow); the Hub [exit fee](#fees) adds friction against racing out ahead of that downward reprice.
|
|
179
|
+
- **A `Failed`-on-under-collateralisation vault pays lender compensation in the collateral token, off the Hub's books.** If fundraising meets its target but the institution has not posted full collateral by `openEndTime`, the vault confiscates the institution's margin and refunds it to suppliers pro-rata in the **collateral asset** (a token distinct from the Hub `asset()`) on each withdrawal, via the vault's `_afterWithdrawHook`. The Hub is the withdrawal receiver, so that collateral lands on the Hub as an unaccounted balance: it is **never** counted in `totalAssets()`, does not move share price, and is recoverable only through the governance [`sweep`](#permissions-asymmetric). This is handled operationally, not in-contract, so no oracle or converter is added to the withdraw hot path. On a default, governance watches the vault's `MarginConfiscated` / `MarginCompensationClaimed` events, sweeps the collateral off the Hub, liquidates it off-chain, and donates the proceeds back to the Hub so the recovery accrues to current LPs through price-per-share. Nothing is at risk while it sits idle, since the balance is fully sweep-recoverable.
|
|
180
|
+
- Because `maxDeposit()` is a view it cannot advance state, so it can report stale non-zero capacity for a vault that is time-due to leave `Fundraising`; a permissionless `updateVaultState()` poke resolves it. This affects only the accuracy of the view, not fund safety.
|
|
181
|
+
|
|
182
|
+
## totalAssets computation
|
|
183
|
+
|
|
184
|
+
Drives share pricing for every deposit/withdraw (`convertToShares` / `convertToAssets`).
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
Hub.totalAssets()
|
|
188
|
+
= Σ YieldGroup.totalAssets() + Hub idle balance
|
|
189
|
+
│
|
|
190
|
+
└─ YieldGroup.totalAssets()
|
|
191
|
+
= Σ adapter.totalAssets(resource, yieldGroup) + YieldGroup idle balance
|
|
192
|
+
```
|
|
193
|
+
|
|
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 gates_ (`maxDeposit` / `maxWithdraw`) 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
|
+
- 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
|
+
- YieldGroup idle accumulates from treasury-fee gross-up surplus and dust handling on past withdrawals; it is counted and consumed idle-first.
|
|
197
|
+
- 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.
|
|
198
|
+
|
|
199
|
+
### Recovering a bricked YieldGroup (governance)
|
|
200
|
+
|
|
201
|
+
A YieldGroup is **"bricked"** when its own `totalAssets()` call reverts. This comes only from a bug in the YieldGroup — **not** from normal market trouble. A paused or empty market, a locked FRV, or a paused token all show up in `withdraw`/`deposit`, never in `totalAssets()`, so the Hub just skips that YieldGroup and keeps running. This section is only for the rare bricked-view case.
|
|
202
|
+
|
|
203
|
+
**The Hub freezes by itself.** `totalAssets()` prices every share and it fails closed, so the instant a YieldGroup's view reverts, every `deposit` / `mint` / `withdraw` / `redeem` reverts too. Governance does not have to do anything to stop trading — it has already stopped. The only question is how to get back to a correct price.
|
|
204
|
+
|
|
205
|
+
**Two ways to recover:**
|
|
206
|
+
|
|
207
|
+
1. **YieldGroup can be fixed → just wait; do nothing.** When `totalAssets()` works again, the Hub un-freezes on its own at the correct price. Nothing moved, nobody lost anything. This is the normal, safest path.
|
|
208
|
+
2. **YieldGroup is permanently dead (funds truly gone) → remove it.** Call `removeYieldGroup(bricked)` (allowed even though its balance read reverts). The Hub then runs at the lower value — which is now the _real_ value, since the money is actually gone — so unpausing is fair and the loss is shared evenly across all current LPs.
|
|
209
|
+
|
|
210
|
+
**Why pause, if the brick already froze everything?** Pausing is not what stops trading — the revert already did that. When a YieldGroup's `totalAssets()` reverts, call `pauseHub()` to turn that freeze into a **clear, intentional halt**: instead of every call failing with a confusing low-level YieldGroup revert, users and integrators see `HubPaused`, and the `max*` views return `0` instead of reverting.
|
|
211
|
+
|
|
212
|
+
> **Why not just `try/catch` the revert, or remove-then-re-add later?** Both do the same unsafe thing — drop the YieldGroup's funds out of the price, then bring them back. While the funds are "out," the price is too low: redeemers are underpaid and depositors buy in cheap, and when the value returns those cheap depositors profit at everyone else's expense. `try/catch` did this automatically on every revert (that is exactly why it was removed); a manual remove-then-re-add does it by hand. So if you ever remove a YieldGroup that still holds funds, keep the Hub **paused** for the whole remove → fix → re-add window, verify the re-added balance off-chain, and restore its withdraw-queue slot before unpausing.
|
|
213
|
+
|
|
214
|
+
## Fees
|
|
215
|
+
|
|
216
|
+
Three fee levers. The **management** and **performance** fees are minted as **dilution shares** to a single `feeRecipient` (`address(0)` disables minting); accrual is idempotent within a block and runs before every deposit/withdraw/reallocate, and both rates are capped at `MAX_FEE_BPS = 50%`. The **exit fee** is separate — it is retained in the vault rather than minted to anyone.
|
|
217
|
+
|
|
218
|
+
- **Management fee** — linear time proration: `totalAssets × bps × Δt / (10_000 × 365 days)`. A single accrual is capped at 40% of TVL; when the cap binds, the uncharged remainder **defers** to the next accrual instead of being forfeited.
|
|
219
|
+
- **Performance fee** — charged only on gains in price-per-share above a monotonic **high-water mark** (`totalAssets × 1e18 / totalSupply`). The HWM advances whenever PPS climbs — independent of whether a fee is minted — so a later rate increase cannot retroactively claim past gains.
|
|
220
|
+
- **Exit fee** (`redeemFeeBps`, capped at `MAX_REDEEM_FEE_BPS = 5%`) — a per-withdraw/redeem fee carved out of the gross and **retained in the vault** (not routed out, not minted to `feeRecipient`), so it lifts price-per-share for the LPs who stay. It is friction against racing out ahead of a downward settlement reprice (e.g. an FRV settling below its accrued mark on a shortfall). The withdraw path advances the HWM by the retained uplift so the performance fee never double-counts it as a gain.
|
|
221
|
+
|
|
222
|
+
v1 launches at `0/0/0`; the machinery exists for governance to enable later.
|
|
223
|
+
|
|
224
|
+
## Multi-level pause
|
|
225
|
+
|
|
226
|
+
Three independent scopes — a broader scope blocks everything beneath it; siblings keep operating. Pause is **asymmetric**: tightening (pause) is operator-accessible, loosening (unpause) is governance-only.
|
|
227
|
+
|
|
228
|
+
```mermaid
|
|
229
|
+
flowchart TB
|
|
230
|
+
classDef paused fill:#ffebee,stroke:#c62828,color:#000,font-weight:bold
|
|
231
|
+
classDef normal fill:#e8f5e9,stroke:#2e7d32,color:#000
|
|
232
|
+
|
|
233
|
+
H["Hub pause<br/>blocks ALL user-facing ops"]:::paused
|
|
234
|
+
H --> S1["YieldGroup pause (Hub flag)<br/>skipped in routing"]:::paused
|
|
235
|
+
H --> S2["YieldGroup B (unaffected)"]:::normal
|
|
236
|
+
S1 --> R1["Resource pause (YieldGroup flag)<br/>skipped in inner queue"]:::paused
|
|
237
|
+
S1 --> R2["Resource 2 (unaffected)"]:::normal
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- **Hub paused** — all deposits / withdrawals / mints / redeems, `reallocate`, fee accrual, and the fee setters are blocked; `emergencyReallocate` and `sweep` stay callable; views stay readable. Underlying products keep operating. Accrual is skipped across the pause window so LPs are never charged for frozen time.
|
|
241
|
+
- **YieldGroup paused** — the Hub-level flag makes routing skip the YieldGroup. Its balance still counts in `totalAssets()`; it is reachable only via `reallocate` / `emergencyReallocate`. New deposit/push legs revert `YieldGroupPaused`.
|
|
242
|
+
- **Resource paused** — the YieldGroup-level flag makes the inner queue skip the resource. Balance still counts. `depositResource` into it reverts; `withdrawResource` from it is allowed (wind-down). There is no global YieldGroup pause — the only granularity inside a YieldGroup is per-resource.
|
|
243
|
+
|
|
244
|
+
## Cap enforcement
|
|
245
|
+
|
|
246
|
+
Caps bind at two levels.
|
|
247
|
+
|
|
248
|
+
**YieldGroup level (Hub) — dual cap.** Each YieldGroup carries an absolute amount AND a percentage of Hub TVL; the stricter binds:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
effectiveCap = (percentageCapBps == 10_000)
|
|
252
|
+
? absoluteCap
|
|
253
|
+
: min(absoluteCap, percentageCapBps × hubTVL / 10_000)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`percentageCapBps == 10_000` (100%) is a sentinel that **disables** the percentage component — required so a fresh Hub at TVL = 0 can take its first deposit (otherwise `pct × 0` collapses the cap to zero). `absoluteCap == type(uint256).max` is rejected; use the sentinel to disable the percentage dimension instead.
|
|
257
|
+
|
|
258
|
+
**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
|
+
|
|
260
|
+
```
|
|
261
|
+
room = min( protocolHeadroom, resourceCap − ourBalanceInResource )
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- `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
|
+
- 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.
|
|
267
|
+
- **YieldGroupFRV has no per-resource cap** — FRV capacity comes entirely from the vault's own `maxBorrowCap` and its Fundraising-only rule.
|
|
268
|
+
|
|
269
|
+
**Per-transaction withdrawal cap (Hub).** `maxWithdrawalSize` (asset units) bounds every single withdraw/redeem; exceeding it reverts `HubWithdrawCapExceeded`.
|
|
270
|
+
|
|
271
|
+
## Permissions (asymmetric)
|
|
272
|
+
|
|
273
|
+
Every privileged call is gated by `AccessControlManagerV8` — the role is `keccak256(targetContract, roleString)`, where `roleString` is the literal function signature shown below. The contracts do not hard-code an operator-vs-governance branch; the asymmetry is entirely a matter of which addresses governance grants each role to. In v1 the Operator is the Venus Core multisig.
|
|
274
|
+
|
|
275
|
+
| Action class | Governance (VIP) | Operator |
|
|
276
|
+
| -------------------------------------------------------------------------------------------- | :----------------: | :----------------: |
|
|
277
|
+
| Add / remove YieldGroup or Resource, **unpause**, set fees, `updateResourceAdapter`, `sweep` | ✅ | ❌ |
|
|
278
|
+
| **Raise** caps, `emergencyReallocate` | ✅ (also Guardian) | ❌ |
|
|
279
|
+
| **Lower** caps, lower per-tx cap, **pause**, reorder queues | ✅ | ✅ |
|
|
280
|
+
| `reallocate` between YieldGroups / resources | — | ✅ |
|
|
281
|
+
| `pauseHub` | ✅ | ✅ (also Guardian) |
|
|
282
|
+
| `deposit` / `mint` / `withdraw` / `redeem`, `accrueFees`, views | permissionless | permissionless |
|
|
283
|
+
|
|
284
|
+
**ACM role strings.** Hub: `addYieldGroup(address,uint256,uint16)`, `removeYieldGroup(address)`, `raiseYieldGroupCap(...)`, `lowerYieldGroupCap(...)`, `setOuterDepositQueue(address[])`, `setOuterWithdrawQueue(address[])`, `pauseHub()`, `unpauseHub()`, `pauseYieldGroup(address)`, `unpauseYieldGroup(address)`, `raiseMaxWithdrawalSize(uint256)`, `lowerMaxWithdrawalSize(uint256)`, `setManagementFeeBps(uint16)`, `setPerformanceFeeBps(uint16)`, `setRedeemFeeBps(uint16)`, `setFeeRecipient(address)`, `sweep(address,address)`, `reallocate(...)`, `emergencyReallocate(...)`. Each YieldGroup: `addResource(address,address)`, `removeResource(address)`, `updateResourceAdapter(address,address)` (governance-only, repoints a resource's delegatecall adapter), `setInnerDepositQueue(address[])`, `setInnerWithdrawQueue(address[])`, `pauseResource(address)`, `unpauseResource(address)`, `sweep(address,address)`, and — Core & Flux only — `raiseResourceCap(address,uint256)`, `lowerResourceCap(address,uint256)`, `setBlocksPerYear(uint256)`; and — FRV only — `forceRemoveResource(address)`.
|
|
285
|
+
|
|
286
|
+
## Invariants & safety
|
|
287
|
+
|
|
288
|
+
- **Atomic-or-revert.** Deposits, withdrawals, and reallocate fully complete or revert with a named error — no partial fills, no remainder returned.
|
|
289
|
+
- **Net-zero reallocate.** `Σ withdraws == Σ deposits`; the Operator can only move funds among registered routes, never in or out.
|
|
290
|
+
- **Stateless adapters.** Adapters declare zero storage (only immutables), never `sstore`, and call only the resource, its `underlying()`, and its `comptroller()`. Mutating entry points revert outside a delegatecall context.
|
|
291
|
+
- **Fault isolation.** A YieldGroup with a reverting `totalAssets()` view contributes 0 instead of bricking the Hub; it can still be removed as an emergency eviction.
|
|
292
|
+
- **Removal safety.** `removeResource` / `removeYieldGroup` gate on a _raw receipt-token_ balance (not underlying value) so a sub-unit residual can't be rounded to zero and orphan tokens.
|
|
293
|
+
- **Inflation defense.** ERC-4626 decimals offset (≤ 12) is set per asset at init; the deploy path is responsible for a non-zero value.
|
|
294
|
+
- **Standard ERC-20 only.** Fee-on-transfer, deflationary, and rebasing underlyings are unsupported (deposits fail closed if a token delivers less than requested).
|
|
295
|
+
- **Upgrade-safe storage.** Each contract isolates its state in a `*Storage` base with a reserved `__gap`; never reorder or prepend fields.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Repository layout
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
contracts/
|
|
303
|
+
Hub/ Hub.sol, HubStorage.sol, lib/HubAdminLib.sol
|
|
304
|
+
YieldGroup/
|
|
305
|
+
core/ YieldGroupCore + Storage, Adapters/AdapterCoreV1
|
|
306
|
+
flux/ YieldGroupFlux + Storage, Adapters/AdapterFlux
|
|
307
|
+
FRV/ YieldGroupFRV + Storage, Adapters/AdapterFRV
|
|
308
|
+
migrator/ Migrator.sol (immutable Core→Hub one-click migration)
|
|
309
|
+
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–07 + DeployBase + config/<network>.json
|
|
312
|
+
tests/foundry/ unit (Hub/, YieldGroup/{core,flux,frv}/, Registry/), Integration/, fork/
|
|
313
|
+
deployments/<network>/ committed registry of deployed addresses (source of truth)
|
|
314
|
+
lib/ git-submodule dependencies
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Build & test
|
|
318
|
+
|
|
319
|
+
Pure **Foundry** — no Hardhat. Node/Yarn is only used for tooling (husky, prettier, solhint, semantic-release). Solidity deps are vendored as git submodules, so clone with submodules first.
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
git submodule update --init --recursive # or: forge install
|
|
323
|
+
forge build --sizes # yarn build
|
|
324
|
+
forge test -vvv # yarn test
|
|
325
|
+
forge coverage --report lcov # yarn coverage
|
|
326
|
+
yarn lint:sol # solhint
|
|
327
|
+
forge fmt # formatting
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- **Compiler:** solc `0.8.25`, optimizer on (200 runs), `via_ir = false`, EVM `cancun`.
|
|
331
|
+
- **Profiles:** `default` (light), `ci` (fuzz 1024 / invariant 256×32), `intense` (fuzz 10000 / invariant 1024×64) — select with `FOUNDRY_PROFILE`.
|
|
332
|
+
- **Submodules:** `forge-std`, `openzeppelin-contracts`, `openzeppelin-contracts-upgradeable`, `@venusprotocol/governance-contracts`, `@venusprotocol/solidity-utilities`.
|
|
333
|
+
|
|
334
|
+
**Test tiers:** unit (per Hub and per YieldGroup family), integration lifecycle over the real Hub + real adapters against faithful mocks, a stateful invariant fuzz (asserts _backed_ / _non-dilutive_ / _conservative_; FRV excluded so its state machine doesn't starve coverage), and BSC-mainnet **fork** tests that reuse the real deploy scripts against live Venus Core, Fluid, and FRV.
|
|
335
|
+
|
|
336
|
+
## Deployment
|
|
337
|
+
|
|
338
|
+
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.
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
01_DeployHubBeacon 02_DeployCore 03_DeployFRV 04_DeployFlux # once per chain (impl + adapter + beacon)
|
|
342
|
+
05_DeployHubRegistry # once per chain (upgradeable singleton, discovery root)
|
|
343
|
+
06_DeployAssetVault # per asset, idempotent
|
|
344
|
+
07_DeployMigrator # once per chain (immutable, permissionless periphery)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
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.**
|
|
348
|
+
|
|
349
|
+
Script 05 deploys the **HubRegistry** (implementation + `TransparentUpgradeableProxy` behind a governance-owned `ProxyAdmin`) — the single on-chain discovery root: indexers watch its `HubAdded`/`HubRemoved` events, then follow each Hub's own `YieldGroupAdded`/`YieldGroupRemoved`. It also starts the registry's `Ownable2Step` transfer to `governance` (completed with `acceptOwnership()` in the onboarding VIP), so the deployer never keeps the owner key that gates `setAccessControlManager`.
|
|
350
|
+
|
|
351
|
+
Script 06 deploys a Hub proxy per asset (YieldGroups read `IHub.asset()` and revert on mismatch), then the three YieldGroup proxies, all initialized through their beacons, and starts each Hub's `Ownable2Step` transfer to `governance` (completed with `acceptOwnership()` in the onboarding VIP — the deployer never keeps the owner key). It refuses assets the registry already serves. Every Hub it deploys is **registered by governance via `hubRegistry.addHub(hub)`** — ordered before the Hub's `addYieldGroup` calls in the same onboarding VIP, so `HubAdded` precedes every `YieldGroupAdded` in one atomic transaction. For Hubs configured before registration (e.g. pre-registry deployments), indexers seed the YieldGroup set from state (`registeredYieldGroups()`), since those events predate `HubAdded`.
|
|
352
|
+
|
|
353
|
+
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.
|
|
354
|
+
|
|
355
|
+
### Hub onboarding flow (deploy + one-tx registration VIP)
|
|
356
|
+
|
|
357
|
+
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.
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
PHASE 1 · Deployer EOA (deploy-only, no roles — scripts in numeric order)
|
|
361
|
+
─────────────────────────────────────────────────────────────────────────
|
|
362
|
+
01_DeployHubBeacon ──► HubBeacon ─┐
|
|
363
|
+
02_DeployCore ──► CoreBeacon ├─ beacons (owner → timelock)
|
|
364
|
+
03_DeployFRV ──► FRVBeacon ─┘
|
|
365
|
+
05_DeployHubRegistry ──► HubRegistry proxy (init: acm; owner → timelock)
|
|
366
|
+
│
|
|
367
|
+
▼
|
|
368
|
+
06_DeployAssetVault
|
|
369
|
+
├─ checks registry.hubForAsset(asset) == 0 (no shadow second Hub)
|
|
370
|
+
├─ new BeaconProxy ──► Hub (owner = EOA)
|
|
371
|
+
├─ Hub.transferOwnership(timelock) (pendingOwner = timelock)
|
|
372
|
+
└─ new BeaconProxy ×3 ──► Core / FRV / Flux YieldGroups (bound to Hub)
|
|
373
|
+
|
|
374
|
+
Hub is LIVE but: unregistered, unwired, owner-pending
|
|
375
|
+
│
|
|
376
|
+
▼
|
|
377
|
+
PHASE 2 · Governance (propose → vote → queue → execute)
|
|
378
|
+
────────────────────────────────────────────────────────
|
|
379
|
+
VIP proposal = ordered call array; the vips-repo fork simulation gates it
|
|
380
|
+
before execution, asserting: HubAdded.logIndex < min(YieldGroupAdded
|
|
381
|
+
.logIndex) per Hub, registry.owner() == timelock, Hub.owner() == timelock,
|
|
382
|
+
accessControlManager == canonical ACM on the registry and every Hub
|
|
383
|
+
(deployer is the LIVE owner until acceptOwnership — this catches an ACM
|
|
384
|
+
swapped in the deploy→VIP window), isHub, group count.
|
|
385
|
+
|
|
386
|
+
GovernorBravo.execute(id) ◄══ ONE TRANSACTION ══►
|
|
387
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
388
|
+
│ [0] acm.giveCallPermission(registry, "addHub(address)", ...) │
|
|
389
|
+
│ [1] acm.giveCallPermission(hub, "addYieldGroup(...)", ...) │
|
|
390
|
+
│ [2] registry.acceptOwnership() ──► registry owner = timelock │
|
|
391
|
+
│ [3] registry.addHub(hub) ──► HubAdded log i │
|
|
392
|
+
│ [4] hub.acceptOwnership() ──► owner = timelock │
|
|
393
|
+
│ [5] hub.addYieldGroup(core, …) ──► YieldGroupAdded log i+1 │
|
|
394
|
+
│ [6] hub.addYieldGroup(frv, …) ──► YieldGroupAdded log i+2 │
|
|
395
|
+
│ [7] addResource / queue configuration … │
|
|
396
|
+
│ │
|
|
397
|
+
│ any step reverts ──► WHOLE tx reverts (no partial state) │
|
|
398
|
+
└───────────────────────────────────────────────────────────────┘
|
|
399
|
+
│
|
|
400
|
+
▼
|
|
401
|
+
PHASE 3 · Indexer (single discovery root)
|
|
402
|
+
──────────────────────────────────────────
|
|
403
|
+
registry: HubAdded (log i)
|
|
404
|
+
│ spawns the Hub data-source template
|
|
405
|
+
▼
|
|
406
|
+
same tx, higher log index: YieldGroupAdded ×N ── caught in order
|
|
407
|
+
│
|
|
408
|
+
└─ NORMATIVE: the HubAdded handler MUST state-seed
|
|
409
|
+
hub.registeredYieldGroups() — event ordering is a latency
|
|
410
|
+
optimization; the state-seed is what guarantees correctness
|
|
411
|
+
(and covers Hubs configured pre-registration)
|
|
412
|
+
|
|
413
|
+
Result: asset ──► hub ──► [core, frv, flux] reconstructed from one address
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Ordering is guaranteed by the call-array index (`addHub` before any `addYieldGroup`), atomicity by EVM revert semantics (one `execute` transaction), and both are pinned by the fork simulation before the proposal can execute. On remote chains (Omnichain executor) the per-chain batch executes as one ordered transaction per chain, so the property holds chain-by-chain.
|
|
417
|
+
|
|
418
|
+
**Reconciliation:** a Hub is live the moment script 06 runs but discoverable only after its VIP — `deploy/ReconcileHubRegistry.s.sol` (read-only, unnumbered) diffs the committed `deployments/<network>/Hub_*.json` set against `registry.getHubs()` in both directions and reverts on any mismatch (shadow Hubs deployed-but-unregistered, or registered-but-untracked). Run it after every onboarding VIP and periodically from monitoring.
|
|
419
|
+
|
|
420
|
+
`deploy/config/<network>.json` holds only public addresses and parameters — `acm`, `governance`, `fluxLendingResolver`, optional `wbnb` / `vBNB` (the Migrator's native path; omit or zero to disable it), and per-asset `key` / `asset` / `name` / `symbol` / `decimalsOffset` / `initialMaxWithdrawalSize` / `blocksPerYear` / `feeRecipient`. The committed `bscmainnet.json` ships with `0x0` placeholders for governance/acm/resolver/recipient and must be filled before a real deploy. Secrets (deployer key, RPC URLs, Etherscan key) live only in `.env` (see `.env.example`). Deployed addresses are committed under `deployments/<network>/<name>.json`; `broadcast/` is gitignored and is not the source of truth.
|
|
421
|
+
|
|
422
|
+
**Networks:** RPC aliases are pre-wired for `bsc_mainnet`, `bsc_testnet`, `ethereum`, `sepolia` (one Etherscan V2 key across chains). v1 targets BNB Chain.
|