@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { IHub } from "../interfaces/IHub.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title HubStorage
|
|
8
|
+
* @notice Hub state, isolated in a base contract so future upgrades can extend without
|
|
9
|
+
* disturbing the inheritance linearisation. Add new fields after `__gap` is shrunk
|
|
10
|
+
* by the same number of slots; never reorder, never prepend.
|
|
11
|
+
*
|
|
12
|
+
* Slot layout (Hub's own contribution; upstream contracts have their own slots
|
|
13
|
+
* before this):
|
|
14
|
+
*
|
|
15
|
+
* Slot Used (bytes) Field
|
|
16
|
+
* ---- ------------ -----
|
|
17
|
+
* 0 1+1 _hubPaused, _decimalsOffsetStored
|
|
18
|
+
* 1 32 _maxWithdrawalSize
|
|
19
|
+
* 2 32 _registeredYieldGroups
|
|
20
|
+
* 3 32 _yieldGroupIndex
|
|
21
|
+
* 4 32 _yieldGroups
|
|
22
|
+
* 5 32 _outerDepositQueue
|
|
23
|
+
* 6 32 _outerWithdrawQueue
|
|
24
|
+
* 7 2+2+20+2 _managementFeeBps, _performanceFeeBps, _feeRecipient, _redeemFeeBps
|
|
25
|
+
* 8 32 _highWaterMarkPerShare
|
|
26
|
+
* 9 8+8+8 _lastFeeAccrualTimestamp, _pauseStart, _lastFeeAccrualBlock
|
|
27
|
+
* 10..49 32 × 40 __gap
|
|
28
|
+
*/
|
|
29
|
+
abstract contract HubStorage {
|
|
30
|
+
// Slot 0: lifecycle + decimals offset.
|
|
31
|
+
|
|
32
|
+
/// @notice True while the Hub is paused.
|
|
33
|
+
bool internal _hubPaused;
|
|
34
|
+
|
|
35
|
+
/// @notice ERC-4626 inflation-defense offset; returned by `_decimalsOffset()`. Set once at init.
|
|
36
|
+
uint8 internal _decimalsOffsetStored;
|
|
37
|
+
|
|
38
|
+
// Slot 1: per-tx withdraw cap.
|
|
39
|
+
|
|
40
|
+
/// @notice Max asset units any single withdraw / redeem can move out of the Hub.
|
|
41
|
+
uint256 internal _maxWithdrawalSize;
|
|
42
|
+
|
|
43
|
+
// Slot 2-4: YieldGroup registry.
|
|
44
|
+
|
|
45
|
+
/// @notice Canonical set of registered YieldGroups.
|
|
46
|
+
address[] internal _registeredYieldGroups;
|
|
47
|
+
|
|
48
|
+
/// @notice 1-indexed position in `_registeredYieldGroups` (0 = not registered). Enables O(1) removal.
|
|
49
|
+
mapping(address yieldGroup => uint256 indexPlusOne) internal _yieldGroupIndex;
|
|
50
|
+
|
|
51
|
+
/// @notice Per-YieldGroup config (caps, pause, registered flag).
|
|
52
|
+
mapping(address yieldGroup => IHub.YieldGroupConfig config) internal _yieldGroups;
|
|
53
|
+
|
|
54
|
+
// Slot 5-6: routing queues.
|
|
55
|
+
|
|
56
|
+
/// @notice Deposit cascading order. Set by Operator.
|
|
57
|
+
address[] internal _outerDepositQueue;
|
|
58
|
+
|
|
59
|
+
/// @notice Withdraw pulling order. Independent of the deposit queue.
|
|
60
|
+
address[] internal _outerWithdrawQueue;
|
|
61
|
+
|
|
62
|
+
// Slot 7: fee parameters (packed).
|
|
63
|
+
|
|
64
|
+
/// @notice Management fee rate in BPS.
|
|
65
|
+
uint16 internal _managementFeeBps;
|
|
66
|
+
|
|
67
|
+
/// @notice Performance fee rate in BPS.
|
|
68
|
+
uint16 internal _performanceFeeBps;
|
|
69
|
+
|
|
70
|
+
/// @notice Recipient of newly-minted fee shares.
|
|
71
|
+
address internal _feeRecipient;
|
|
72
|
+
|
|
73
|
+
/// @notice Exit (withdraw/redeem) fee in BPS, RETAINED in the vault (socialised to remaining
|
|
74
|
+
/// LPs as a price-per-share uplift, not minted to a recipient). Default 0; a governance
|
|
75
|
+
/// anti sandwich lever for discrete settlement YieldGroups - friction against racing out
|
|
76
|
+
/// ahead of a downward settlement reprice (e.g. an FRV settling below its accrued mark on a
|
|
77
|
+
/// shortfall). Packed into the existing fee slot, so it consumes no additional storage slot.
|
|
78
|
+
uint16 internal _redeemFeeBps;
|
|
79
|
+
|
|
80
|
+
// Slot 8: performance HWM.
|
|
81
|
+
|
|
82
|
+
/// @notice High-water mark in share-price units (`totalAssets × 1e18 / totalSupply`).
|
|
83
|
+
uint256 internal _highWaterMarkPerShare;
|
|
84
|
+
|
|
85
|
+
// Slot 9: fee-accrual cursors.
|
|
86
|
+
|
|
87
|
+
/// @notice Block timestamp of last fee accrual. Drives management-fee proration only (elapsed
|
|
88
|
+
/// wall-clock time); it is not the per-block idempotence key (see `_lastFeeAccrualBlock`).
|
|
89
|
+
uint64 internal _lastFeeAccrualTimestamp;
|
|
90
|
+
|
|
91
|
+
/// @notice Block timestamp when the Hub was paused. `unpauseHub` advances
|
|
92
|
+
/// `_lastFeeAccrualTimestamp` by the pause duration so the frozen window is excluded
|
|
93
|
+
/// from management-fee accrual. Packed into slot 9 alongside `_lastFeeAccrualTimestamp`,
|
|
94
|
+
/// so it consumes no additional storage slot.
|
|
95
|
+
uint64 internal _pauseStart;
|
|
96
|
+
|
|
97
|
+
/// @notice Block number of last fee accrual, the once-per-block idempotence key for `_accrueFees`.
|
|
98
|
+
/// Resource NAV (a Core vToken's `exchangeRateStored`) is block-lazy: it can only change
|
|
99
|
+
/// when `block.number` advances. Keying idempotence off the block, not the timestamp,
|
|
100
|
+
/// means a fresh block always refreshes resources even when two consecutive blocks share a
|
|
101
|
+
/// timestamp (routine on sub-second chains). Packed into slot 9, so it adds no slot.
|
|
102
|
+
uint64 internal _lastFeeAccrualBlock;
|
|
103
|
+
|
|
104
|
+
/// @dev Reserved slots for upgrades. Shrink by the number of slots a new field consumes.
|
|
105
|
+
uint256[40] private __gap;
|
|
106
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
|
|
6
|
+
import { IHub } from "../../interfaces/IHub.sol";
|
|
7
|
+
import { IYieldGroupBase } from "../../interfaces/IYieldGroupBase.sol";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @title HubAdminLib
|
|
11
|
+
* @author Venus
|
|
12
|
+
* @notice Governance/operator-only ("cold") registry and queue logic for the {Hub}, extracted
|
|
13
|
+
* into a delegatecall-linked library to keep the Hub implementation under the EIP-170
|
|
14
|
+
* 24,576-byte limit.
|
|
15
|
+
* @dev Every function is invoked via DELEGATECALL from the Hub, so `address(this)`, the storage
|
|
16
|
+
* slots reached through the passed `storage` references, and the event emitter all remain
|
|
17
|
+
* the Hub. The logic here is a near-verbatim relocation of the former in-Hub bodies, and
|
|
18
|
+
* emitted events are unchanged. Only `removeYieldGroup` differs: its gate now nets the
|
|
19
|
+
* YieldGroup's idle asset balance out of `totalAssets()`. The library holds NO state, so
|
|
20
|
+
* the Hub's storage layout (see {HubStorage}) and upgrade-safety are unaffected. Access
|
|
21
|
+
* control (`_checkAccessAllowed`) stays in the Hub's thin wrappers, never here.
|
|
22
|
+
*/
|
|
23
|
+
library HubAdminLib {
|
|
24
|
+
/// @notice BPS denominator (`10_000` = 100%); mirrors `Hub.BPS_DENOMINATOR`.
|
|
25
|
+
uint16 internal constant BPS_DENOMINATOR = 10_000;
|
|
26
|
+
|
|
27
|
+
// ============================== YieldGroup registry ===========================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @notice Add a YieldGroup to the registry with initial caps. Verbatim relocation of the former
|
|
31
|
+
* `Hub.addYieldGroup` body.
|
|
32
|
+
* @param yieldGroups Hub's per-YieldGroup config mapping (storage ref).
|
|
33
|
+
* @param registeredYieldGroups Hub's registry array (storage ref).
|
|
34
|
+
* @param yieldGroupIndex Hub's 1-indexed position mapping (storage ref).
|
|
35
|
+
* @param yieldGroup YieldGroup contract address (must implement IYieldGroupBase).
|
|
36
|
+
* @param absoluteCap Initial absolute cap in asset units.
|
|
37
|
+
* @param percentageCapBps Initial percentage cap in BPS (≤ 10_000).
|
|
38
|
+
* @param hubAsset The Hub's own `asset()`, supplied by the wrapper for the match check.
|
|
39
|
+
*/
|
|
40
|
+
function addYieldGroup(
|
|
41
|
+
mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
|
|
42
|
+
address[] storage registeredYieldGroups,
|
|
43
|
+
mapping(address => uint256) storage yieldGroupIndex,
|
|
44
|
+
address yieldGroup,
|
|
45
|
+
uint256 absoluteCap,
|
|
46
|
+
uint16 percentageCapBps,
|
|
47
|
+
address hubAsset
|
|
48
|
+
) external {
|
|
49
|
+
if (yieldGroup == address(0)) revert IHub.ZeroAddress();
|
|
50
|
+
if (yieldGroups[yieldGroup].registered) revert IHub.YieldGroupAlreadyRegistered(yieldGroup);
|
|
51
|
+
if (percentageCapBps > BPS_DENOMINATOR || absoluteCap == type(uint256).max) {
|
|
52
|
+
revert IHub.InvalidCap(absoluteCap, percentageCapBps);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
address yieldGroupAsset = IYieldGroupBase(yieldGroup).asset();
|
|
56
|
+
if (yieldGroupAsset != hubAsset) revert IHub.YieldGroupAssetMismatch(yieldGroup, yieldGroupAsset, hubAsset);
|
|
57
|
+
|
|
58
|
+
registeredYieldGroups.push(yieldGroup);
|
|
59
|
+
yieldGroupIndex[yieldGroup] = registeredYieldGroups.length;
|
|
60
|
+
yieldGroups[yieldGroup] = IHub.YieldGroupConfig({
|
|
61
|
+
absoluteCap: absoluteCap, percentageCapBps: percentageCapBps, paused: false, registered: true
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
emit IHub.YieldGroupAdded(yieldGroup, absoluteCap, percentageCapBps);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @notice Remove a YieldGroup from the registry: swap-and-pop the registry array and
|
|
69
|
+
* cascade-strip the YieldGroup from both outer queues.
|
|
70
|
+
* @param yieldGroups Hub's per-YieldGroup config mapping (storage ref).
|
|
71
|
+
* @param registeredYieldGroups Hub's registry array (storage ref).
|
|
72
|
+
* @param yieldGroupIndex Hub's 1-indexed position mapping (storage ref).
|
|
73
|
+
* @param outerDepositQueue Hub's deposit queue (storage ref).
|
|
74
|
+
* @param outerWithdrawQueue Hub's withdraw queue (storage ref).
|
|
75
|
+
* @param yieldGroup YieldGroup to remove.
|
|
76
|
+
* @param hubAsset The Hub's own `asset()`, supplied by the wrapper to net idle balance out of the gate.
|
|
77
|
+
*/
|
|
78
|
+
function removeYieldGroup(
|
|
79
|
+
mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
|
|
80
|
+
address[] storage registeredYieldGroups,
|
|
81
|
+
mapping(address => uint256) storage yieldGroupIndex,
|
|
82
|
+
address[] storage outerDepositQueue,
|
|
83
|
+
address[] storage outerWithdrawQueue,
|
|
84
|
+
address yieldGroup,
|
|
85
|
+
address hubAsset
|
|
86
|
+
) external {
|
|
87
|
+
IHub.YieldGroupConfig storage cfg = yieldGroups[yieldGroup];
|
|
88
|
+
if (!cfg.registered) revert IHub.YieldGroupNotRegistered(yieldGroup);
|
|
89
|
+
|
|
90
|
+
// Normal path: refuse removal while the YieldGroup still custodies resource-backed value.
|
|
91
|
+
// `totalAssets()` reports that value plus any idle asset-token balance sitting directly on the
|
|
92
|
+
// YieldGroup. The idle term is netted out here: anyone can push dust straight to a drained
|
|
93
|
+
// YieldGroup, and counting it would let a 1-wei transfer block deregistration indefinitely.
|
|
94
|
+
// Residual idle is handled out-of-band.
|
|
95
|
+
//
|
|
96
|
+
// Emergency path: if the YieldGroup's own `totalAssets()` reverts (a bricked / buggy
|
|
97
|
+
// YieldGroup), removal is permitted anyway so a misbehaving YieldGroup can be evicted without
|
|
98
|
+
// a Hub upgrade, again with any residual value handled out-of-band.
|
|
99
|
+
bool balanceReadable;
|
|
100
|
+
uint256 yieldGroupBal;
|
|
101
|
+
try IYieldGroupBase(yieldGroup).totalAssets() returns (uint256 v) {
|
|
102
|
+
balanceReadable = true;
|
|
103
|
+
// Net out idle asset held directly by the YieldGroup. Only YieldGroupBase defines
|
|
104
|
+
// `totalAssets()` (resource-backed value + this idle balance), so the difference is
|
|
105
|
+
// exactly the resource-backed value. `v >= idle` always holds; the guard is defensive.
|
|
106
|
+
uint256 idle = IERC20(hubAsset).balanceOf(yieldGroup);
|
|
107
|
+
yieldGroupBal = v > idle ? v - idle : 0;
|
|
108
|
+
} catch {
|
|
109
|
+
balanceReadable = false;
|
|
110
|
+
}
|
|
111
|
+
if (balanceReadable && yieldGroupBal != 0) revert IHub.YieldGroupHasBalance(yieldGroup, yieldGroupBal);
|
|
112
|
+
|
|
113
|
+
uint256 idxPlusOne = yieldGroupIndex[yieldGroup];
|
|
114
|
+
uint256 idx = idxPlusOne - 1;
|
|
115
|
+
uint256 lastIdx = registeredYieldGroups.length - 1;
|
|
116
|
+
if (idx != lastIdx) {
|
|
117
|
+
address last = registeredYieldGroups[lastIdx];
|
|
118
|
+
registeredYieldGroups[idx] = last;
|
|
119
|
+
yieldGroupIndex[last] = idxPlusOne;
|
|
120
|
+
}
|
|
121
|
+
registeredYieldGroups.pop();
|
|
122
|
+
delete yieldGroupIndex[yieldGroup];
|
|
123
|
+
|
|
124
|
+
_removeFromQueue(outerDepositQueue, yieldGroup);
|
|
125
|
+
_removeFromQueue(outerWithdrawQueue, yieldGroup);
|
|
126
|
+
|
|
127
|
+
delete yieldGroups[yieldGroup];
|
|
128
|
+
|
|
129
|
+
emit IHub.YieldGroupRemoved(yieldGroup);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @notice Raise or lower a YieldGroup's caps. Merges the former symmetric `Hub.raiseYieldGroupCap`
|
|
134
|
+
* and `Hub.lowerYieldGroupCap` bodies behind a direction flag; the shrink/grow guard and
|
|
135
|
+
* the emitted event are selected by `raising`. Behavior is identical to the originals.
|
|
136
|
+
* @param yieldGroups Hub's per-YieldGroup config mapping (storage ref).
|
|
137
|
+
* @param yieldGroup YieldGroup whose caps are being changed.
|
|
138
|
+
* @param absoluteCap New absolute cap in asset units.
|
|
139
|
+
* @param percentageCapBps New percentage cap in BPS.
|
|
140
|
+
* @param raising True for the raise path (reverts NotIncreasing), false for lower (NotDecreasing).
|
|
141
|
+
*/
|
|
142
|
+
function setYieldGroupCap(
|
|
143
|
+
mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
|
|
144
|
+
address yieldGroup,
|
|
145
|
+
uint256 absoluteCap,
|
|
146
|
+
uint16 percentageCapBps,
|
|
147
|
+
bool raising
|
|
148
|
+
) external {
|
|
149
|
+
IHub.YieldGroupConfig storage cfg = yieldGroups[yieldGroup];
|
|
150
|
+
if (!cfg.registered) revert IHub.YieldGroupNotRegistered(yieldGroup);
|
|
151
|
+
if (percentageCapBps > BPS_DENOMINATOR || absoluteCap == type(uint256).max) {
|
|
152
|
+
revert IHub.InvalidCap(absoluteCap, percentageCapBps);
|
|
153
|
+
}
|
|
154
|
+
// Reject the wrong-direction move AND a full no-op: at least one dimension must strictly
|
|
155
|
+
// change in the requested direction, so a successful call always changes state and the
|
|
156
|
+
// emitted event is never spurious.
|
|
157
|
+
if (raising) {
|
|
158
|
+
if (
|
|
159
|
+
absoluteCap < cfg.absoluteCap || percentageCapBps < cfg.percentageCapBps
|
|
160
|
+
|| (absoluteCap == cfg.absoluteCap && percentageCapBps == cfg.percentageCapBps)
|
|
161
|
+
) {
|
|
162
|
+
revert IHub.NotIncreasing();
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
if (
|
|
166
|
+
absoluteCap > cfg.absoluteCap || percentageCapBps > cfg.percentageCapBps
|
|
167
|
+
|| (absoluteCap == cfg.absoluteCap && percentageCapBps == cfg.percentageCapBps)
|
|
168
|
+
) {
|
|
169
|
+
revert IHub.NotDecreasing();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
cfg.absoluteCap = absoluteCap;
|
|
174
|
+
cfg.percentageCapBps = percentageCapBps;
|
|
175
|
+
if (raising) {
|
|
176
|
+
emit IHub.YieldGroupCapRaised(yieldGroup, absoluteCap, percentageCapBps);
|
|
177
|
+
} else {
|
|
178
|
+
emit IHub.YieldGroupCapLowered(yieldGroup, absoluteCap, percentageCapBps);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================== Outer queues ==============================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @notice Validate a candidate deposit routing queue (every entry registered, no duplicates).
|
|
186
|
+
* The caller (Hub) performs the storage assignment + event, since a calldata array
|
|
187
|
+
* cannot be copied through a storage-pointer parameter here.
|
|
188
|
+
* @param yieldGroups Hub's per-YieldGroup config mapping (storage ref) for the registered check.
|
|
189
|
+
* @param queue Candidate queue in deposit-priority order.
|
|
190
|
+
*/
|
|
191
|
+
function validateDepositQueue(
|
|
192
|
+
mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
|
|
193
|
+
address[] calldata queue
|
|
194
|
+
) external view {
|
|
195
|
+
_validateQueue(yieldGroups, queue);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @notice Validate a candidate withdraw routing queue: valid (registered, no duplicates) AND
|
|
200
|
+
* covering every funded registered YieldGroup. The caller (Hub) performs the storage
|
|
201
|
+
* assignment + event.
|
|
202
|
+
* @param yieldGroups Hub's per-YieldGroup config mapping (storage ref) for the registered check.
|
|
203
|
+
* @param registeredYieldGroups Hub's registry array (storage ref) for the funded-coverage check.
|
|
204
|
+
* @param queue Candidate queue in withdraw-priority order.
|
|
205
|
+
*/
|
|
206
|
+
function validateWithdrawQueue(
|
|
207
|
+
mapping(address => IHub.YieldGroupConfig) storage yieldGroups,
|
|
208
|
+
address[] storage registeredYieldGroups,
|
|
209
|
+
address[] calldata queue
|
|
210
|
+
) external view {
|
|
211
|
+
_validateQueue(yieldGroups, queue);
|
|
212
|
+
_requireWithdrawQueueCoversFundedYieldGroups(registeredYieldGroups, queue);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================== Internal helpers ==========================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @notice Remove the first occurrence of `target` from `queue` via swap-and-pop.
|
|
219
|
+
* @param queue Storage array to mutate.
|
|
220
|
+
* @param target Address to remove (no-op if absent).
|
|
221
|
+
*/
|
|
222
|
+
function _removeFromQueue(address[] storage queue, address target) private {
|
|
223
|
+
uint256 len = queue.length;
|
|
224
|
+
for (uint256 i; i < len; ++i) {
|
|
225
|
+
if (queue[i] == target) {
|
|
226
|
+
if (i != len - 1) {
|
|
227
|
+
queue[i] = queue[len - 1];
|
|
228
|
+
}
|
|
229
|
+
queue.pop();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @notice Revert if the candidate withdraw queue omits any registered YieldGroup that still
|
|
237
|
+
* custodies a balance. A YieldGroup whose `totalAssets()` reverts is treated as funded.
|
|
238
|
+
* @param registeredYieldGroups Hub's registry array (storage ref).
|
|
239
|
+
* @param queue Candidate withdraw queue.
|
|
240
|
+
*/
|
|
241
|
+
function _requireWithdrawQueueCoversFundedYieldGroups(
|
|
242
|
+
address[] storage registeredYieldGroups,
|
|
243
|
+
address[] calldata queue
|
|
244
|
+
) private view {
|
|
245
|
+
uint256 rlen = registeredYieldGroups.length;
|
|
246
|
+
for (uint256 i; i < rlen; ++i) {
|
|
247
|
+
address yieldGroup = registeredYieldGroups[i];
|
|
248
|
+
bool funded;
|
|
249
|
+
try IYieldGroupBase(yieldGroup).totalAssets() returns (uint256 v) {
|
|
250
|
+
funded = v > 0;
|
|
251
|
+
} catch {
|
|
252
|
+
funded = true;
|
|
253
|
+
}
|
|
254
|
+
if (funded && !_queueContains(queue, yieldGroup)) {
|
|
255
|
+
revert IHub.WithdrawQueueOmitsFundedYieldGroup(yieldGroup);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @notice Validate a routing queue: every entry registered, no duplicates.
|
|
262
|
+
* @param yieldGroups Hub's per-YieldGroup config mapping (storage ref).
|
|
263
|
+
* @param queue Candidate queue.
|
|
264
|
+
*/
|
|
265
|
+
function _validateQueue(mapping(address => IHub.YieldGroupConfig) storage yieldGroups, address[] calldata queue)
|
|
266
|
+
private
|
|
267
|
+
view
|
|
268
|
+
{
|
|
269
|
+
uint256 len = queue.length;
|
|
270
|
+
for (uint256 i; i < len; ++i) {
|
|
271
|
+
address yieldGroup = queue[i];
|
|
272
|
+
if (!yieldGroups[yieldGroup].registered) revert IHub.YieldGroupNotRegistered(yieldGroup);
|
|
273
|
+
for (uint256 j = i + 1; j < len; ++j) {
|
|
274
|
+
if (queue[j] == yieldGroup) revert IHub.InvalidQueue();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @notice Linear membership test over a calldata address array.
|
|
281
|
+
* @param queue Array to scan.
|
|
282
|
+
* @param target Address to find.
|
|
283
|
+
* @return found True iff `target` appears in `queue`.
|
|
284
|
+
*/
|
|
285
|
+
function _queueContains(address[] calldata queue, address target) private pure returns (bool found) {
|
|
286
|
+
uint256 len = queue.length;
|
|
287
|
+
for (uint256 i; i < len; ++i) {
|
|
288
|
+
if (queue[i] == target) return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
|
|
5
|
+
import { IYieldGroup } from "../interfaces/IYieldGroup.sol";
|
|
6
|
+
import { IYieldGroupBase } from "../interfaces/IYieldGroupBase.sol";
|
|
7
|
+
import { YieldGroupBase } from "./base/YieldGroupBase.sol";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @title YieldGroup
|
|
11
|
+
* @author Venus
|
|
12
|
+
* @notice The generic concrete YieldGroup, shared by the synchronous families. "Core" (Venus
|
|
13
|
+
* vTokens) and "Flux" (Fluid fTokens) are the same contract behind different beacons; a
|
|
14
|
+
* family is a *deployment identity* — which adapter + resources + per-resource caps +
|
|
15
|
+
* `blocksPerYear` value get wired into the proxy — not a code difference.
|
|
16
|
+
* @dev Adds to {YieldGroupBase}: the optional per-resource deposit cap (the cap-aware
|
|
17
|
+
* `_resourceDepositRoom` override + its raise/lower governance surface), the `blocksPerYear`
|
|
18
|
+
* annualiser used by `spotAPYBps`, and the `accrue()` poke loop that settles block-lazy
|
|
19
|
+
* interest before the Hub reads NAV. Lifecycle families (FRV) use {YieldGroupFRV} instead.
|
|
20
|
+
* Inheritance order is load-bearing for upgrade safety — do not reorder.
|
|
21
|
+
*/
|
|
22
|
+
contract YieldGroup is YieldGroupBase, IYieldGroup {
|
|
23
|
+
// ============================== External — mutating =======================
|
|
24
|
+
// -- Initialisation --
|
|
25
|
+
|
|
26
|
+
/// @inheritdoc IYieldGroup
|
|
27
|
+
function initialize(address hub_, address asset_, uint256 blocksPerYear_, address acm_) external initializer {
|
|
28
|
+
__YieldGroupBase_init(hub_, asset_, blocksPerYear_, acm_);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// -- Resource caps (asymmetric) --
|
|
32
|
+
|
|
33
|
+
/// @inheritdoc IYieldGroup
|
|
34
|
+
function raiseResourceCap(address resource, uint256 newCap) external override {
|
|
35
|
+
_checkAccessAllowed("raiseResourceCap(address,uint256)");
|
|
36
|
+
if (!_resourcesConfig[resource].registered) revert ResourceNotRegistered(resource);
|
|
37
|
+
// `0` = unbounded, so compare in effective terms: a raise must be strictly looser.
|
|
38
|
+
if (_effectiveResourceCap(newCap) <= _effectiveResourceCap(_resourceCap[resource])) {
|
|
39
|
+
revert NotIncreasing();
|
|
40
|
+
}
|
|
41
|
+
_resourceCap[resource] = newCap;
|
|
42
|
+
emit ResourceCapRaised(resource, newCap);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// @inheritdoc IYieldGroup
|
|
46
|
+
function lowerResourceCap(address resource, uint256 newCap) external override {
|
|
47
|
+
_checkAccessAllowed("lowerResourceCap(address,uint256)");
|
|
48
|
+
if (!_resourcesConfig[resource].registered) revert ResourceNotRegistered(resource);
|
|
49
|
+
// A lower must set a real (non-zero) cap strictly tighter than the current one;
|
|
50
|
+
// `0` means unbounded, which is a raise, not a lower.
|
|
51
|
+
if (newCap == 0 || _effectiveResourceCap(newCap) >= _effectiveResourceCap(_resourceCap[resource])) {
|
|
52
|
+
revert NotDecreasing();
|
|
53
|
+
}
|
|
54
|
+
_resourceCap[resource] = newCap;
|
|
55
|
+
emit ResourceCapLowered(resource, newCap);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// -- Blocks-per-year annualiser (governance) --
|
|
59
|
+
|
|
60
|
+
/// @inheritdoc IYieldGroup
|
|
61
|
+
function setBlocksPerYear(uint256 newBlocksPerYear) external override {
|
|
62
|
+
_checkAccessAllowed("setBlocksPerYear(uint256)");
|
|
63
|
+
uint256 old = _blocksPerYear;
|
|
64
|
+
_blocksPerYear = newBlocksPerYear;
|
|
65
|
+
emit BlocksPerYearSet(old, newBlocksPerYear);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// -- IYieldGroupBase: accrue --
|
|
69
|
+
|
|
70
|
+
/// @inheritdoc IYieldGroupBase
|
|
71
|
+
/// @dev Pokes every registered resource (the same set {totalAssets} sums, paused included) to
|
|
72
|
+
/// settle interest up to this block. Best-effort: a resource whose accrual reverts is
|
|
73
|
+
/// skipped and left at its last-settled rate rather than bubbling and bricking the Hub's
|
|
74
|
+
/// fee accrual. Resources whose NAV is a live preview (Fluid) make each call a no-op.
|
|
75
|
+
function accrue() external override(YieldGroupBase, IYieldGroupBase) onlyHub {
|
|
76
|
+
address[] storage list = _resources;
|
|
77
|
+
uint256 len = list.length;
|
|
78
|
+
for (uint256 i; i < len; ++i) {
|
|
79
|
+
address adapter = _resourcesConfig[list[i]].adapter;
|
|
80
|
+
try IResourceAdapter(adapter).accrue(list[i]) { }
|
|
81
|
+
catch {
|
|
82
|
+
emit ResourceAccrualFailed(list[i], adapter);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================== External — view ==========================
|
|
88
|
+
|
|
89
|
+
/// @inheritdoc IYieldGroup
|
|
90
|
+
function blocksPerYear() external view override returns (uint256) {
|
|
91
|
+
return _blocksPerYear;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// @inheritdoc IYieldGroup
|
|
95
|
+
function resourceCap(address resource) external view override returns (uint256) {
|
|
96
|
+
return _resourceCap[resource];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================== Internal — view ==========================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @inheritdoc YieldGroupBase
|
|
103
|
+
* @dev Clamps the protocol-level headroom by this YieldGroup's optional per-resource cap.
|
|
104
|
+
* When no cap is set (`0`), returns the protocol headroom directly and skips the balance
|
|
105
|
+
* lookup entirely — the cap feature is zero-cost when unused. When set, the operator
|
|
106
|
+
* headroom is `cap − our current balance` (floored at zero).
|
|
107
|
+
*/
|
|
108
|
+
function _resourceDepositRoom(address resource, address adapter) internal view override returns (uint256 room) {
|
|
109
|
+
uint256 protocolRoom = IResourceAdapter(adapter).maxDeposit(resource);
|
|
110
|
+
uint256 cap = _resourceCap[resource];
|
|
111
|
+
if (cap == 0) return protocolRoom;
|
|
112
|
+
|
|
113
|
+
uint256 held = IResourceAdapter(adapter).totalAssets(resource, address(this));
|
|
114
|
+
uint256 operatorRoom = cap > held ? cap - held : 0;
|
|
115
|
+
return protocolRoom < operatorRoom ? protocolRoom : operatorRoom;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @notice Map the stored cap sentinel to a comparable value: `0` (unset) becomes the max
|
|
120
|
+
* uint, so an unbounded cap sorts above every finite cap in the raise/lower
|
|
121
|
+
* comparisons and never binds in the deposit-room math.
|
|
122
|
+
* @param storedCap Raw value from `_resourceCap`.
|
|
123
|
+
* @return effective `type(uint256).max` when `storedCap == 0`, else `storedCap`.
|
|
124
|
+
*/
|
|
125
|
+
function _effectiveResourceCap(uint256 storedCap) private pure returns (uint256 effective) {
|
|
126
|
+
return storedCap == 0 ? type(uint256).max : storedCap;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
|
|
5
|
+
import { IYieldGroupBase } from "../interfaces/IYieldGroupBase.sol";
|
|
6
|
+
import { IYieldGroupFRV } from "../interfaces/IYieldGroupFRV.sol";
|
|
7
|
+
import { IFRVVault } from "../interfaces/external/IFRVVault.sol";
|
|
8
|
+
import { YieldGroupBase } from "./base/YieldGroupBase.sol";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @title YieldGroupFRV
|
|
12
|
+
* @author Venus
|
|
13
|
+
* @notice Per-asset YieldGroup that aggregates one or more Venus Fixed Rate Vaults behind the
|
|
14
|
+
* standard `IYieldGroupBase` Hub boundary; deployed behind a proxy, one per (Hub, asset).
|
|
15
|
+
* @dev A thin subclass of {YieldGroupBase}. FRV-specific concerns, all expressed by overriding the
|
|
16
|
+
* base's virtual hooks:
|
|
17
|
+
* - Each vault has a lifecycle state machine (`Fundraising → Lock → Matured / Closed`).
|
|
18
|
+
* `_prepareResource` advances it via `vault.updateVaultState()` before every
|
|
19
|
+
* deposit/withdraw sizing read, so capacity / liquidity reflect current state.
|
|
20
|
+
* - `_minDeposit` returns the vault's `minSupplierDeposit`, so a sub-floor non-tail deposit
|
|
21
|
+
* is skipped (cascade) or rejected cleanly (targeted) instead of failing inside the vault.
|
|
22
|
+
* FRV has no per-resource cap (it inherits the base's raw `_resourceDepositRoom`) and no
|
|
23
|
+
* `blocksPerYear` (it initialises it to `0`; `AdapterFRV` ignores the annualiser). It adds the
|
|
24
|
+
* `forceRemoveResource` escape hatch for orphaning a permanently un-drainable (Closed) vault.
|
|
25
|
+
* Inheritance order is load-bearing for upgrade safety — do not reorder.
|
|
26
|
+
*/
|
|
27
|
+
contract YieldGroupFRV is YieldGroupBase, IYieldGroupFRV {
|
|
28
|
+
// ============================== External — mutating =======================
|
|
29
|
+
// -- Initialisation --
|
|
30
|
+
|
|
31
|
+
/// @inheritdoc IYieldGroupFRV
|
|
32
|
+
function initialize(address hub_, address asset_, address acm_) external initializer {
|
|
33
|
+
// FRV has no per-block rate to annualise, so `blocksPerYear` is 0.
|
|
34
|
+
__YieldGroupBase_init(hub_, asset_, 0, acm_);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// -- Resource registry (governance) --
|
|
38
|
+
|
|
39
|
+
// -- IYieldGroupBase: accrue --
|
|
40
|
+
|
|
41
|
+
/// @inheritdoc IYieldGroupBase
|
|
42
|
+
/// @dev Poke every registered vault's state so the Hub's NAV read sees the current state, not a
|
|
43
|
+
/// stale `Fundraising` (valued at principal, no coupon). Best-effort: a reverting poke is
|
|
44
|
+
/// skipped, not bubbled.
|
|
45
|
+
function accrue() external override(YieldGroupBase, IYieldGroupBase) onlyHub {
|
|
46
|
+
address[] storage list = _resources;
|
|
47
|
+
uint256 len = list.length;
|
|
48
|
+
for (uint256 i; i < len; ++i) {
|
|
49
|
+
try IFRVVault(list[i]).updateVaultState() { }
|
|
50
|
+
catch {
|
|
51
|
+
emit ResourceAccrualFailed(list[i], _resourcesConfig[list[i]].adapter);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// @inheritdoc IYieldGroupFRV
|
|
57
|
+
function forceRemoveResource(address resource) external override {
|
|
58
|
+
_checkAccessAllowed("forceRemoveResource(address)");
|
|
59
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
60
|
+
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
61
|
+
|
|
62
|
+
// A resource may only be force-orphaned once it holds no realizable value (NAV == 0), which
|
|
63
|
+
// for FRV is true only of a Closed vault's residual shares (`AdapterFRV.totalAssets` values
|
|
64
|
+
// a Closed position at 0). Note the gate is NAV, not `maxWithdraw`: a live FRV vault is
|
|
65
|
+
// non-withdrawable (`maxWithdraw == 0`) all through Fundraising, Lock and PendingSettlement
|
|
66
|
+
// while still backing real funds, so withdrawability would wrongly pass a healthy vault.
|
|
67
|
+
uint256 nav = IResourceAdapter(cfg.adapter).totalAssets(resource, address(this));
|
|
68
|
+
if (nav != 0) revert ResourceHasValue(resource, nav);
|
|
69
|
+
|
|
70
|
+
uint256 orphaned = IResourceAdapter(cfg.adapter).receiptBalance(resource, address(this));
|
|
71
|
+
|
|
72
|
+
uint256 idxPlusOne = _resourceIndex[resource];
|
|
73
|
+
uint256 idx = idxPlusOne - 1;
|
|
74
|
+
uint256 lastIdx = _resources.length - 1;
|
|
75
|
+
if (idx != lastIdx) {
|
|
76
|
+
address last = _resources[lastIdx];
|
|
77
|
+
_resources[idx] = last;
|
|
78
|
+
_resourceIndex[last] = idxPlusOne;
|
|
79
|
+
}
|
|
80
|
+
_resources.pop();
|
|
81
|
+
delete _resourceIndex[resource];
|
|
82
|
+
|
|
83
|
+
_removeFromQueue(_innerDepositQueue, resource);
|
|
84
|
+
_removeFromQueue(_innerWithdrawQueue, resource);
|
|
85
|
+
|
|
86
|
+
delete _resourcesConfig[resource];
|
|
87
|
+
|
|
88
|
+
emit ResourceForceRemoved(resource, orphaned);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================== Internal — virtual hooks =================
|
|
92
|
+
|
|
93
|
+
/// @dev Advance the FRV vault's lifecycle state machine before each sizing read. Without this,
|
|
94
|
+
/// a vault past its fundraising window would report stale capacity and the deposit call
|
|
95
|
+
/// inside the adapter would revert instead of gracefully skipping. Invoked outside the
|
|
96
|
+
/// cascade try/catch — a poke failure is a genuinely broken vault, not the skip class.
|
|
97
|
+
function _prepareResource(address resource) internal override {
|
|
98
|
+
IFRVVault(resource).updateVaultState();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// @dev FRV's per-resource minimum deposit floor. A vault rejects a deposit below
|
|
102
|
+
/// `minSupplierDeposit` unless it exactly fills the vault's remaining capacity (the
|
|
103
|
+
/// residual tail), which the base routing handles via this floor.
|
|
104
|
+
function _minDeposit(address resource) internal view override returns (uint256) {
|
|
105
|
+
return IFRVVault(resource).config().minSupplierDeposit;
|
|
106
|
+
}
|
|
107
|
+
}
|