@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,797 @@
|
|
|
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
|
+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
6
|
+
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
|
|
7
|
+
import {
|
|
8
|
+
ReentrancyGuardUpgradeable
|
|
9
|
+
} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
IAccessControlManagerV8
|
|
13
|
+
} from "@venusprotocol/governance-contracts/contracts/Governance/IAccessControlManagerV8.sol";
|
|
14
|
+
|
|
15
|
+
import { IHub } from "../../interfaces/IHub.sol";
|
|
16
|
+
import { IYieldGroupBase } from "../../interfaces/IYieldGroupBase.sol";
|
|
17
|
+
import { IResourceAdapter } from "../../interfaces/IResourceAdapter.sol";
|
|
18
|
+
import { YieldGroupBaseStorage } from "./YieldGroupBaseStorage.sol";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @title YieldGroupBase
|
|
22
|
+
* @author Venus
|
|
23
|
+
* @notice Shared base for every concrete YieldGroup — the generic `YieldGroup` (Core / Flux) and
|
|
24
|
+
* the `YieldGroupFRV` subclass. Holds the entire registry / queue / pause / deposit /
|
|
25
|
+
* withdraw / view machinery behind the standard `IYieldGroupBase` Hub boundary; deployed
|
|
26
|
+
* behind a proxy, one per (Hub, asset).
|
|
27
|
+
* @dev Architecture: Hub → YieldGroup* → IResourceAdapter (via delegatecall) → resource.
|
|
28
|
+
* The YieldGroup is **ABI-agnostic**: every protocol-specific concern (mint signature,
|
|
29
|
+
* treasury-fee gross-up, supply-cap math, APY annualisation) lives in the adapter.
|
|
30
|
+
* Inheritance order is load-bearing for upgrade safety — do not reorder.
|
|
31
|
+
*
|
|
32
|
+
* Family divergence is captured by four `internal virtual` hooks, all defaulting here to the
|
|
33
|
+
* simplest behavior; a concrete overrides only what its family needs:
|
|
34
|
+
* - `_prepareResource` (mutating) — advance a resource's internal state before sizing
|
|
35
|
+
* (FRV: `updateVaultState()`); default no-op.
|
|
36
|
+
* - `_minDeposit` (view) — per-resource minimum deposit floor (FRV:
|
|
37
|
+
* `minSupplierDeposit`); default 0.
|
|
38
|
+
* - `_resourceDepositRoom` (view) — spare deposit room (generic: cap-aware); default the
|
|
39
|
+
* raw protocol headroom.
|
|
40
|
+
* - `accrue` (external) — settle interest (generic: poke loop); default no-op.
|
|
41
|
+
*
|
|
42
|
+
* Stateless shared adapters: each registered resource carries an adapter address. The
|
|
43
|
+
* adapter is invoked via DELEGATECALL for mutating functions, so receipt tokens are
|
|
44
|
+
* credited to / burned from this YieldGroup directly (`msg.sender` to the underlying
|
|
45
|
+
* protocol is this contract, not the adapter). One adapter deployment per ABI family
|
|
46
|
+
* serves every YieldGroup that registers a matching resource — no per-asset adapter
|
|
47
|
+
* instances, no proxies, no clones.
|
|
48
|
+
*
|
|
49
|
+
* Adding a new ABI family later (e.g. Core_V2): deploy a single new stateless adapter
|
|
50
|
+
* and call `addResource(newResource, newAdapter)` on each affected YieldGroup. No
|
|
51
|
+
* changes to this contract, to the Hub, or to existing adapters.
|
|
52
|
+
*
|
|
53
|
+
* Delegatecall safety: adapter implementations MUST follow the storage-safety
|
|
54
|
+
* invariants documented on {IResourceAdapter} (zero state variables, no `sstore` in
|
|
55
|
+
* assembly, only trusted callees). `addResource` rejects EOAs to prevent silent
|
|
56
|
+
* no-op delegatecalls; revert bubbling is handled by `Address.functionDelegateCall`.
|
|
57
|
+
*/
|
|
58
|
+
abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseStorage, IYieldGroupBase {
|
|
59
|
+
using SafeERC20 for IERC20;
|
|
60
|
+
using Address for address;
|
|
61
|
+
|
|
62
|
+
// ============================== Modifiers ================================
|
|
63
|
+
|
|
64
|
+
/// @notice Reverts {NotHub} when the caller is not the registered Hub.
|
|
65
|
+
modifier onlyHub() {
|
|
66
|
+
if (msg.sender != _hub) revert NotHub(msg.sender);
|
|
67
|
+
_;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================== Constructor ==============================
|
|
71
|
+
|
|
72
|
+
/// @custom:oz-upgrades-unsafe-allow constructor
|
|
73
|
+
constructor() {
|
|
74
|
+
_disableInitializers();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================== Internal — initialisation =================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @notice Shared initialiser invoked by each concrete's public `initialize`.
|
|
81
|
+
* @dev Validates `IHub(hub_).asset() == asset_` so a misconfigured deployment cannot
|
|
82
|
+
* route the wrong underlying. There is intentionally no `blocksPerYear_ != 0` guard:
|
|
83
|
+
* `blocksPerYear` only annualises the informational `spotAPYBps` view, and families whose
|
|
84
|
+
* adapter pre-annualises (Flux) or has no per-block rate (FRV) initialise it to `0`.
|
|
85
|
+
* @param hub_ Hub authorised to call `deposit` / `withdraw`.
|
|
86
|
+
* @param asset_ Underlying ERC-20 (must match `Hub.asset()`).
|
|
87
|
+
* @param blocksPerYear_ Per-chain block cadence used only for spot-APY annualisation; `0` when
|
|
88
|
+
* the family's adapter ignores it.
|
|
89
|
+
* @param acm_ AccessControlManagerV8 gating governance calls.
|
|
90
|
+
*/
|
|
91
|
+
function __YieldGroupBase_init(address hub_, address asset_, uint256 blocksPerYear_, address acm_)
|
|
92
|
+
internal
|
|
93
|
+
onlyInitializing
|
|
94
|
+
{
|
|
95
|
+
if (hub_ == address(0) || asset_ == address(0) || acm_ == address(0)) revert ZeroAddress();
|
|
96
|
+
address hubAsset = IHub(hub_).asset();
|
|
97
|
+
if (hubAsset != asset_) revert HubAssetMismatch(asset_, hubAsset);
|
|
98
|
+
|
|
99
|
+
__ReentrancyGuard_init();
|
|
100
|
+
|
|
101
|
+
_hub = hub_;
|
|
102
|
+
_assetToken = asset_;
|
|
103
|
+
_blocksPerYear = blocksPerYear_;
|
|
104
|
+
_accessControlManager = acm_;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================== External — mutating =======================
|
|
108
|
+
// -- Resource registry (governance) --
|
|
109
|
+
|
|
110
|
+
/// @inheritdoc IYieldGroupBase
|
|
111
|
+
function addResource(address resource, address adapter) external override {
|
|
112
|
+
_checkAccessAllowed("addResource(address,address)");
|
|
113
|
+
if (resource == address(0) || adapter == address(0)) revert ZeroAddress();
|
|
114
|
+
if (_resourcesConfig[resource].registered) revert ResourceAlreadyRegistered(resource);
|
|
115
|
+
if (resource.code.length == 0) revert ResourceNotContract(resource);
|
|
116
|
+
// Reject EOAs explicitly: delegatecall to an EOA returns success with empty data,
|
|
117
|
+
// which would silently no-op every deposit/withdraw through this resource.
|
|
118
|
+
if (adapter.code.length == 0) revert AdapterNotContract(adapter);
|
|
119
|
+
|
|
120
|
+
// The adapter is the only contract that knows the resource's ABI; routing
|
|
121
|
+
// asset-match validation through it keeps this YieldGroup family-agnostic.
|
|
122
|
+
address resourceAsset = IResourceAdapter(adapter).asset(resource);
|
|
123
|
+
if (resourceAsset != _assetToken) revert ResourceAssetMismatch(resource, resourceAsset, _assetToken);
|
|
124
|
+
|
|
125
|
+
// Let the adapter veto a resource that fails a protocol-specific precondition — e.g. a
|
|
126
|
+
// Core market whose Comptroller charges a non-zero treasuryPercent, an exit fee that
|
|
127
|
+
// leaves the pool on redeem without burning shares and so is unmodeled in the gross NAV.
|
|
128
|
+
IResourceAdapter(adapter).validateRegistration(resource);
|
|
129
|
+
|
|
130
|
+
_resources.push(resource);
|
|
131
|
+
_resourceIndex[resource] = _resources.length;
|
|
132
|
+
_resourcesConfig[resource] = ResourceConfig({ registered: true, paused: false, adapter: adapter });
|
|
133
|
+
|
|
134
|
+
emit ResourceAdded(resource, adapter);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// @inheritdoc IYieldGroupBase
|
|
138
|
+
function removeResource(address resource) external override {
|
|
139
|
+
_checkAccessAllowed("removeResource(address)");
|
|
140
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
141
|
+
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
142
|
+
|
|
143
|
+
// Gate on the raw receipt-token balance, not the underlying value: a value-based check
|
|
144
|
+
// rounds a sub-unit residual to 0 and would let removal orphan dust receipt tokens.
|
|
145
|
+
uint256 held = IResourceAdapter(cfg.adapter).receiptBalance(resource, address(this));
|
|
146
|
+
if (held != 0) revert ResourceHasBalance(resource, held);
|
|
147
|
+
|
|
148
|
+
uint256 idxPlusOne = _resourceIndex[resource];
|
|
149
|
+
uint256 idx = idxPlusOne - 1;
|
|
150
|
+
uint256 lastIdx = _resources.length - 1;
|
|
151
|
+
if (idx != lastIdx) {
|
|
152
|
+
address last = _resources[lastIdx];
|
|
153
|
+
_resources[idx] = last;
|
|
154
|
+
_resourceIndex[last] = idxPlusOne;
|
|
155
|
+
}
|
|
156
|
+
_resources.pop();
|
|
157
|
+
delete _resourceIndex[resource];
|
|
158
|
+
|
|
159
|
+
_removeFromQueue(_innerDepositQueue, resource);
|
|
160
|
+
_removeFromQueue(_innerWithdrawQueue, resource);
|
|
161
|
+
|
|
162
|
+
// Clear the per-resource cap too: leaving it set would let a later re-`addResource` of the
|
|
163
|
+
// same address silently inherit a stale value, since `addResource` never writes it and the
|
|
164
|
+
// setter is registered-only. Removal restores a clean slate.
|
|
165
|
+
delete _resourceCap[resource];
|
|
166
|
+
delete _resourcesConfig[resource];
|
|
167
|
+
|
|
168
|
+
emit ResourceRemoved(resource);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// @inheritdoc IYieldGroupBase
|
|
172
|
+
function updateResourceAdapter(address resource, address newAdapter) external override {
|
|
173
|
+
_checkAccessAllowed("updateResourceAdapter(address,address)");
|
|
174
|
+
if (newAdapter == address(0)) revert ZeroAddress();
|
|
175
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
176
|
+
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
177
|
+
// Reject EOAs: delegatecall to an EOA succeeds with empty data and silently no-ops routing.
|
|
178
|
+
if (newAdapter.code.length == 0) revert AdapterNotContract(newAdapter);
|
|
179
|
+
|
|
180
|
+
// Validate the new adapter exactly as addResource does; the held balance and registry slot
|
|
181
|
+
// are untouched, only the adapter pointer changes.
|
|
182
|
+
address resourceAsset = IResourceAdapter(newAdapter).asset(resource);
|
|
183
|
+
if (resourceAsset != _assetToken) revert ResourceAssetMismatch(resource, resourceAsset, _assetToken);
|
|
184
|
+
IResourceAdapter(newAdapter).validateRegistration(resource);
|
|
185
|
+
|
|
186
|
+
address oldAdapter = cfg.adapter;
|
|
187
|
+
cfg.adapter = newAdapter;
|
|
188
|
+
|
|
189
|
+
emit ResourceAdapterUpdated(resource, oldAdapter, newAdapter);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// -- Inner queues (Operator) --
|
|
193
|
+
|
|
194
|
+
/// @inheritdoc IYieldGroupBase
|
|
195
|
+
function setInnerDepositQueue(address[] calldata queue) external override {
|
|
196
|
+
_checkAccessAllowed("setInnerDepositQueue(address[])");
|
|
197
|
+
_validateQueue(queue);
|
|
198
|
+
_innerDepositQueue = queue;
|
|
199
|
+
emit InnerDepositQueueSet(queue);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// @inheritdoc IYieldGroupBase
|
|
203
|
+
function setInnerWithdrawQueue(address[] calldata queue) external override {
|
|
204
|
+
_checkAccessAllowed("setInnerWithdrawQueue(address[])");
|
|
205
|
+
_validateQueue(queue);
|
|
206
|
+
_requireWithdrawQueueCoversFundedResources(queue);
|
|
207
|
+
_innerWithdrawQueue = queue;
|
|
208
|
+
emit InnerWithdrawQueueSet(queue);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// -- Pause (asymmetric) --
|
|
212
|
+
|
|
213
|
+
/// @inheritdoc IYieldGroupBase
|
|
214
|
+
function pauseResource(address resource) external override {
|
|
215
|
+
_checkAccessAllowed("pauseResource(address)");
|
|
216
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
217
|
+
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
218
|
+
// Idempotent: pausing an already-paused resource is a silent no-op (mirrors Hub.pauseYieldGroup).
|
|
219
|
+
if (!cfg.paused) {
|
|
220
|
+
cfg.paused = true;
|
|
221
|
+
emit ResourcePauseToggled(resource, true);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// @inheritdoc IYieldGroupBase
|
|
226
|
+
function unpauseResource(address resource) external override {
|
|
227
|
+
_checkAccessAllowed("unpauseResource(address)");
|
|
228
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
229
|
+
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
230
|
+
// Idempotent: unpausing an already-unpaused resource is a silent no-op (mirrors Hub).
|
|
231
|
+
if (cfg.paused) {
|
|
232
|
+
cfg.paused = false;
|
|
233
|
+
emit ResourcePauseToggled(resource, false);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// -- Sweep (governance) --
|
|
238
|
+
|
|
239
|
+
/// @inheritdoc IYieldGroupBase
|
|
240
|
+
/// @dev Bars `asset()` (idle, counted in `totalAssets`) and every registered resource: in all
|
|
241
|
+
/// current families the resource address *is* the receipt token this YieldGroup holds, so
|
|
242
|
+
/// sweeping one would drain the backing. A removed resource is unregistered, so any residual
|
|
243
|
+
/// it left behind is recoverable. Zero balance is a silent no-op.
|
|
244
|
+
function sweep(address token, address to) external override nonReentrant {
|
|
245
|
+
_checkAccessAllowed("sweep(address,address)");
|
|
246
|
+
if (to == address(0)) revert ZeroAddress();
|
|
247
|
+
if (token == _assetToken) revert SweepProtectedAsset();
|
|
248
|
+
if (_resourcesConfig[token].registered) revert SweepProtectedResource(token);
|
|
249
|
+
|
|
250
|
+
uint256 bal = IERC20(token).balanceOf(address(this));
|
|
251
|
+
if (bal > 0) {
|
|
252
|
+
IERC20(token).safeTransfer(to, bal);
|
|
253
|
+
emit Swept(token, to, bal);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -- IYieldGroupBase: deposit / withdraw --
|
|
258
|
+
|
|
259
|
+
/// @inheritdoc IYieldGroupBase
|
|
260
|
+
function deposit(uint256 amount)
|
|
261
|
+
external
|
|
262
|
+
override(IYieldGroupBase)
|
|
263
|
+
onlyHub
|
|
264
|
+
nonReentrant
|
|
265
|
+
returns (uint256 deposited)
|
|
266
|
+
{
|
|
267
|
+
IERC20(_assetToken).safeTransferFrom(msg.sender, address(this), amount);
|
|
268
|
+
|
|
269
|
+
uint256 remaining = amount;
|
|
270
|
+
address[] storage queue = _innerDepositQueue;
|
|
271
|
+
uint256 len = queue.length;
|
|
272
|
+
for (uint256 i; i < len && remaining > 0; ++i) {
|
|
273
|
+
remaining -= _tryDepositOneResource(queue[i], remaining);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Refund the unplaced remainder to the caller and return the amount actually placed.
|
|
277
|
+
// The Hub routes the refund on to the next YieldGroup in its queue.
|
|
278
|
+
if (remaining > 0) IERC20(_assetToken).safeTransfer(msg.sender, remaining);
|
|
279
|
+
return amount - remaining;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// @inheritdoc IYieldGroupBase
|
|
283
|
+
function withdraw(uint256 amount, address to) external override(IYieldGroupBase) onlyHub nonReentrant {
|
|
284
|
+
uint256 remaining = amount;
|
|
285
|
+
|
|
286
|
+
// Use idle first (free, no redeem). Idle accumulates from gross-up surplus on past
|
|
287
|
+
// withdrawals; consuming it before touching a resource keeps balances clean.
|
|
288
|
+
uint256 idle = IERC20(_assetToken).balanceOf(address(this));
|
|
289
|
+
if (idle > 0) {
|
|
290
|
+
uint256 fromIdle = idle < remaining ? idle : remaining;
|
|
291
|
+
IERC20(_assetToken).safeTransfer(to, fromIdle);
|
|
292
|
+
remaining -= fromIdle;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
address[] storage queue = _innerWithdrawQueue;
|
|
296
|
+
uint256 len = queue.length;
|
|
297
|
+
for (uint256 i; i < len && remaining > 0; ++i) {
|
|
298
|
+
remaining -= _tryWithdrawOneResource(queue[i], remaining, to);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (remaining > 0) revert ResourceLiquidityInsufficient(amount, amount - remaining);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// @inheritdoc IYieldGroupBase
|
|
305
|
+
/// @dev Base default: no-op. Block-lazy families (the generic `YieldGroup` serving Core)
|
|
306
|
+
/// override this to poke each registered resource and settle interest up to this block;
|
|
307
|
+
/// preview-NAV families (Flux, FRV) keep the no-op because their value is read live.
|
|
308
|
+
// solhint-disable-next-line no-empty-blocks
|
|
309
|
+
function accrue() external virtual override(IYieldGroupBase) onlyHub { }
|
|
310
|
+
|
|
311
|
+
// -- IYieldGroupBase: resource-targeted reallocation (Operator, via Hub) --
|
|
312
|
+
|
|
313
|
+
/// @inheritdoc IYieldGroupBase
|
|
314
|
+
/// @dev Bypasses the inner deposit queue to place the full `amount` into one resource.
|
|
315
|
+
/// Reverts on under-capacity rather than partial-filling, so the Hub's reallocate
|
|
316
|
+
/// net-zero accounting holds. Refuses paused resources (do not route into a paused
|
|
317
|
+
/// market — mirrors the queue path skipping them). The resource need not be in
|
|
318
|
+
/// `_innerWithdrawQueue`: value placed outside it counts in `totalAssets()` but is not
|
|
319
|
+
/// reached by the withdraw cascade until it is (re)added via {setInnerWithdrawQueue}.
|
|
320
|
+
function depositResource(address resource, uint256 amount)
|
|
321
|
+
external
|
|
322
|
+
override(IYieldGroupBase)
|
|
323
|
+
onlyHub
|
|
324
|
+
nonReentrant
|
|
325
|
+
returns (uint256 deposited)
|
|
326
|
+
{
|
|
327
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
328
|
+
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
329
|
+
if (cfg.paused) revert ResourceIsPaused(resource);
|
|
330
|
+
|
|
331
|
+
IERC20(_assetToken).safeTransferFrom(msg.sender, address(this), amount);
|
|
332
|
+
|
|
333
|
+
_prepareResource(resource);
|
|
334
|
+
|
|
335
|
+
address adapter = cfg.adapter;
|
|
336
|
+
uint256 room = _resourceDepositRoom(resource, adapter);
|
|
337
|
+
if (amount > room) revert ResourceCapacityExceeded(amount, room);
|
|
338
|
+
|
|
339
|
+
// A subclass that imposes a per-resource minimum deposit (e.g. FRV's `minSupplierDeposit`)
|
|
340
|
+
// rejects a sub-minimum amount unless it is the exact residual tail (`amount == room`),
|
|
341
|
+
// which a resource always accepts. Reject it cleanly here rather than bubbling the
|
|
342
|
+
// resource's own opaque error. For the generic `YieldGroup`, `_minDeposit` is 0 and this
|
|
343
|
+
// block never triggers.
|
|
344
|
+
if (amount < room) {
|
|
345
|
+
uint256 minDeposit = _minDeposit(resource);
|
|
346
|
+
if (amount < minDeposit) revert ResourceBelowMinimumDeposit(resource, amount, minDeposit);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_dispatchDeposit(adapter, resource, amount);
|
|
350
|
+
return amount;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// @inheritdoc IYieldGroupBase
|
|
354
|
+
/// @dev Bypasses both the idle-first path and the inner withdraw queue: redeems the full
|
|
355
|
+
/// `amount` from one resource and delivers it to `to`, or reverts. Pulling from a
|
|
356
|
+
/// paused resource is allowed (winding down a paused market is the intended use).
|
|
357
|
+
function withdrawResource(address resource, uint256 amount, address to)
|
|
358
|
+
external
|
|
359
|
+
override(IYieldGroupBase)
|
|
360
|
+
onlyHub
|
|
361
|
+
nonReentrant
|
|
362
|
+
{
|
|
363
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
364
|
+
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
365
|
+
|
|
366
|
+
_prepareResource(resource);
|
|
367
|
+
|
|
368
|
+
address adapter = cfg.adapter;
|
|
369
|
+
uint256 liquid = IResourceAdapter(adapter).maxWithdraw(resource, address(this));
|
|
370
|
+
if (amount > liquid) revert ResourceLiquidityInsufficient(amount, liquid);
|
|
371
|
+
|
|
372
|
+
uint256 received = _dispatchWithdraw(adapter, resource, amount, to);
|
|
373
|
+
if (received != amount) revert AdapterUnderfilled(resource, amount, received);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ============================== External — view ==========================
|
|
377
|
+
|
|
378
|
+
/// @inheritdoc IYieldGroupBase
|
|
379
|
+
function hub() external view override returns (address) {
|
|
380
|
+
return _hub;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// @inheritdoc IYieldGroupBase
|
|
384
|
+
function accessControlManager() external view override returns (address) {
|
|
385
|
+
return _accessControlManager;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// @inheritdoc IYieldGroupBase
|
|
389
|
+
function resources() external view override returns (address[] memory) {
|
|
390
|
+
return _resources;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/// @inheritdoc IYieldGroupBase
|
|
394
|
+
function resourceConfig(address resource)
|
|
395
|
+
external
|
|
396
|
+
view
|
|
397
|
+
override
|
|
398
|
+
returns (bool registered, bool paused, address adapter)
|
|
399
|
+
{
|
|
400
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
401
|
+
return (cfg.registered, cfg.paused, cfg.adapter);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// @inheritdoc IYieldGroupBase
|
|
405
|
+
function innerDepositQueue() external view override returns (address[] memory) {
|
|
406
|
+
return _innerDepositQueue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// @inheritdoc IYieldGroupBase
|
|
410
|
+
function innerWithdrawQueue() external view override returns (address[] memory) {
|
|
411
|
+
return _innerWithdrawQueue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// -- IYieldGroupBase views --
|
|
415
|
+
|
|
416
|
+
/// @inheritdoc IYieldGroupBase
|
|
417
|
+
function asset() external view override(IYieldGroupBase) returns (address) {
|
|
418
|
+
return _assetToken;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/// @inheritdoc IYieldGroupBase
|
|
422
|
+
/// @dev `Σ resource value` (via each resource's adapter) plus any idle asset on the
|
|
423
|
+
/// YieldGroup. Idle accrues from treasury-fee gross-up surplus on past withdrawals.
|
|
424
|
+
/// `virtual` so a future family can override the valuation; today every family (the generic
|
|
425
|
+
/// `YieldGroup` and `YieldGroupFRV`) shares this gross sum of per-resource adapter values.
|
|
426
|
+
function totalAssets() external view virtual override(IYieldGroupBase) returns (uint256 total) {
|
|
427
|
+
address[] storage list = _resources;
|
|
428
|
+
uint256 len = list.length;
|
|
429
|
+
for (uint256 i; i < len; ++i) {
|
|
430
|
+
address resource = list[i];
|
|
431
|
+
total += IResourceAdapter(_resourcesConfig[resource].adapter).totalAssets(resource, address(this));
|
|
432
|
+
}
|
|
433
|
+
total += IERC20(_assetToken).balanceOf(address(this));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/// @inheritdoc IYieldGroupBase
|
|
437
|
+
function maxDeposit() external view override(IYieldGroupBase) returns (uint256 total) {
|
|
438
|
+
address[] storage queue = _innerDepositQueue;
|
|
439
|
+
uint256 len = queue.length;
|
|
440
|
+
for (uint256 i; i < len; ++i) {
|
|
441
|
+
address resource = queue[i];
|
|
442
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
443
|
+
if (cfg.registered && !cfg.paused) {
|
|
444
|
+
uint256 room = _resourceDepositRoom(resource, cfg.adapter);
|
|
445
|
+
// Only advertise capacity the cascade can actually place for any amount <= it. A
|
|
446
|
+
// subclass with a per-resource minimum deposit only accepts a sub-floor amount as
|
|
447
|
+
// the exact residual tail; reporting room below that floor would break the
|
|
448
|
+
// `deposit(maxDeposit())` honesty contract, so report 0 for such a resource. For
|
|
449
|
+
// the generic `YieldGroup`, `_minDeposit` is 0 and `room` always passes.
|
|
450
|
+
uint256 minDep = _minDeposit(resource);
|
|
451
|
+
// solhint-disable-next-line gas-strict-inequalities
|
|
452
|
+
total += room >= minDep ? room : 0;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/// @inheritdoc IYieldGroupBase
|
|
458
|
+
/// @dev Includes idle (deliverable without redeem) and each unpaused queue entry's
|
|
459
|
+
/// net-of-protocol-fee redeemable amount.
|
|
460
|
+
function maxWithdraw() external view override(IYieldGroupBase) returns (uint256 total) {
|
|
461
|
+
address[] storage queue = _innerWithdrawQueue;
|
|
462
|
+
uint256 len = queue.length;
|
|
463
|
+
for (uint256 i; i < len; ++i) {
|
|
464
|
+
address resource = queue[i];
|
|
465
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
466
|
+
if (cfg.registered && !cfg.paused) {
|
|
467
|
+
total += IResourceAdapter(cfg.adapter).maxWithdraw(resource, address(this));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
total += IERC20(_assetToken).balanceOf(address(this));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/// @inheritdoc IYieldGroupBase
|
|
474
|
+
/// @dev TVL-weighted across registered resources. Each adapter returns its own spot APY;
|
|
475
|
+
/// we weight by underlying value held. Saturates at `uint64.max` to defend against
|
|
476
|
+
/// a misreporting adapter.
|
|
477
|
+
function spotAPYBps() external view override(IYieldGroupBase) returns (uint64) {
|
|
478
|
+
uint256 totalVal;
|
|
479
|
+
uint256 weighted;
|
|
480
|
+
address[] storage list = _resources;
|
|
481
|
+
uint256 len = list.length;
|
|
482
|
+
uint256 bpy = _blocksPerYear;
|
|
483
|
+
for (uint256 i; i < len; ++i) {
|
|
484
|
+
address resource = list[i];
|
|
485
|
+
address adapter = _resourcesConfig[resource].adapter;
|
|
486
|
+
uint256 resVal = IResourceAdapter(adapter).totalAssets(resource, address(this));
|
|
487
|
+
if (resVal > 0) {
|
|
488
|
+
uint256 apy = IResourceAdapter(adapter).spotAPYBps(resource, bpy);
|
|
489
|
+
weighted += apy * resVal;
|
|
490
|
+
totalVal += resVal;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (totalVal == 0) return 0;
|
|
494
|
+
uint256 spot = weighted / totalVal;
|
|
495
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
496
|
+
return spot > type(uint64).max ? type(uint64).max : uint64(spot);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ============================== Internal — view ==========================
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @notice ACM check used by every governance-gated entry point.
|
|
503
|
+
* @param signature ACM role string to authorise against `msg.sender`.
|
|
504
|
+
*/
|
|
505
|
+
function _checkAccessAllowed(string memory signature) internal view {
|
|
506
|
+
if (!IAccessControlManagerV8(_accessControlManager).isAllowedToCall(msg.sender, signature)) {
|
|
507
|
+
revert Unauthorized(msg.sender, signature);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ============================== Private — mutating =======================
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* @notice Try to deposit up to `remaining` into one resource via its adapter. Skips
|
|
515
|
+
* unregistered / paused.
|
|
516
|
+
* @dev Delegatecalls the adapter so receipt tokens (e.g. vTokens) are credited to this
|
|
517
|
+
* YieldGroup, not the adapter. `Address.functionDelegateCall` validates the adapter
|
|
518
|
+
* is a contract and bubbles any revert reason.
|
|
519
|
+
* @param resource Resource to deposit into.
|
|
520
|
+
* @param remaining Underlying units still needing placement.
|
|
521
|
+
* @return placed Underlying units placed into `resource`.
|
|
522
|
+
*/
|
|
523
|
+
function _tryDepositOneResource(address resource, uint256 remaining) private returns (uint256 placed) {
|
|
524
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
525
|
+
if (!cfg.registered || cfg.paused) return 0;
|
|
526
|
+
|
|
527
|
+
// Advance the resource's internal state (FRV: `updateVaultState()`) BEFORE sizing, so the
|
|
528
|
+
// room read reflects current state. Stays OUTSIDE the dispatch try/catch below — a poke
|
|
529
|
+
// failure is a genuinely broken resource, not the route-around-on-revert class.
|
|
530
|
+
_prepareResource(resource);
|
|
531
|
+
|
|
532
|
+
address adapter = cfg.adapter;
|
|
533
|
+
uint256 room = _resourceDepositRoom(resource, adapter);
|
|
534
|
+
uint256 toDeposit = remaining < room ? remaining : room;
|
|
535
|
+
if (toDeposit == 0) return 0;
|
|
536
|
+
|
|
537
|
+
// A subclass with a per-resource minimum deposit skips a sub-floor leg that is not the exact
|
|
538
|
+
// residual tail (`toDeposit == room`), so the cascade continues to the next resource instead
|
|
539
|
+
// of reverting with the resource's own minimum-deposit error. For the generic `YieldGroup`,
|
|
540
|
+
// `_minDeposit` is 0 and this is dead.
|
|
541
|
+
if (toDeposit < room && toDeposit < _minDeposit(resource)) return 0;
|
|
542
|
+
|
|
543
|
+
// Route around a resource whose deposit reverts (e.g. its external protocol paused it after
|
|
544
|
+
// the views certified capacity) instead of bubbling. The adapter's allowance is set inside
|
|
545
|
+
// the delegatecall and rolls back atomically on revert, so nothing to reset here. A skip
|
|
546
|
+
// returns 0, so the cascade's backstop still reverts the whole op when no healthy resource
|
|
547
|
+
// can absorb the full amount.
|
|
548
|
+
try this.dispatchDepositExternal(adapter, resource, toDeposit) returns (uint256 p) {
|
|
549
|
+
return p;
|
|
550
|
+
} catch {
|
|
551
|
+
emit ResourceSkipped(resource, true);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* @notice Try to withdraw up to `remaining` from one resource via its adapter. Skips
|
|
558
|
+
* unregistered / paused.
|
|
559
|
+
* @dev Delegatecalls the adapter so `redeemUnderlying` (or equivalent) burns this
|
|
560
|
+
* YieldGroup's receipt tokens and credits underlying to `to`. The recipient's balance
|
|
561
|
+
* delta is measured and, in the success branch (outside the skip `catch`), asserted equal
|
|
562
|
+
* to `toDeliver`; a shortfall reverts {AdapterUnderfilled} and halts the whole withdraw
|
|
563
|
+
* rather than being silently routed around.
|
|
564
|
+
* @param resource Resource to pull from.
|
|
565
|
+
* @param remaining Underlying units still needing collection.
|
|
566
|
+
* @param to Recipient of the underlying.
|
|
567
|
+
* @return delivered Underlying units delivered to `to` from this resource.
|
|
568
|
+
*/
|
|
569
|
+
function _tryWithdrawOneResource(address resource, uint256 remaining, address to)
|
|
570
|
+
private
|
|
571
|
+
returns (uint256 delivered)
|
|
572
|
+
{
|
|
573
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
574
|
+
if (!cfg.registered || cfg.paused) return 0;
|
|
575
|
+
|
|
576
|
+
// Advance the resource's internal state (FRV: `updateVaultState()`, which may unlock funds
|
|
577
|
+
// via a lifecycle transition) before reading liquidity. Outside the dispatch try/catch.
|
|
578
|
+
_prepareResource(resource);
|
|
579
|
+
|
|
580
|
+
address adapter = cfg.adapter;
|
|
581
|
+
uint256 liquid = IResourceAdapter(adapter).maxWithdraw(resource, address(this));
|
|
582
|
+
uint256 toDeliver = remaining < liquid ? remaining : liquid;
|
|
583
|
+
if (toDeliver == 0) return 0;
|
|
584
|
+
|
|
585
|
+
// Skip-on-revert for the withdraw leg (see `_tryDepositOneResource`): an external-protocol
|
|
586
|
+
// revert (pause, cap, reentrancy-blocked leg) lands in `catch` and routes around this
|
|
587
|
+
// resource. Under-delivery is OUR own accounting invariant, not a route-around case, so it
|
|
588
|
+
// is asserted in the success branch below; a revert there is not caught by the `catch` and
|
|
589
|
+
// propagates, halting the whole withdraw loudly. This mirrors the Hub's own YieldGroup
|
|
590
|
+
// underfill guard in `_tryWithdrawOneYieldGroup` and needs no error-selector matching.
|
|
591
|
+
try this.dispatchWithdrawExternal(adapter, resource, toDeliver, to) returns (uint256 received) {
|
|
592
|
+
if (received != toDeliver) revert AdapterUnderfilled(resource, toDeliver, received);
|
|
593
|
+
return received;
|
|
594
|
+
} catch {
|
|
595
|
+
emit ResourceSkipped(resource, false);
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* @notice Delegatecall the adapter to deposit exactly `amount` into `resource` and emit
|
|
602
|
+
* {DepositRouted}.
|
|
603
|
+
* @dev Shared by the inner-queue deposit path and the resource-targeted reallocate path.
|
|
604
|
+
* Callers own the registration / pause / capacity checks and must ensure this
|
|
605
|
+
* YieldGroup already holds `amount` of underlying. Delegatecall credits receipt
|
|
606
|
+
* tokens to this YieldGroup; `Address.functionDelegateCall` bubbles adapter reverts.
|
|
607
|
+
* @param adapter Adapter bound to `resource`.
|
|
608
|
+
* @param resource Resource to deposit into.
|
|
609
|
+
* @param amount Underlying units to place (exact).
|
|
610
|
+
*/
|
|
611
|
+
function _dispatchDeposit(address adapter, address resource, uint256 amount) private {
|
|
612
|
+
adapter.functionDelegateCall(abi.encodeCall(IResourceAdapter.deposit, (resource, amount)));
|
|
613
|
+
emit DepositRouted(resource, amount);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* @notice Delegatecall the adapter to redeem `amount` from `resource` to `to`, emit
|
|
618
|
+
* {WithdrawRouted}, and return the recipient's measured balance delta.
|
|
619
|
+
* @dev Shared by the inner-queue withdraw path and the resource-targeted reallocate path.
|
|
620
|
+
* Callers own the registration / liquidity checks and the under-delivery invariant: this
|
|
621
|
+
* returns the amount actually received so each caller can assert `received == amount` in
|
|
622
|
+
* its own frame (a shortfall reverts {AdapterUnderfilled}). Deliberately does NOT revert on
|
|
623
|
+
* a shortfall itself — doing so inside the cascade's external self-call frame would blur
|
|
624
|
+
* our own invariant with an external-protocol revert and force selector-matching to tell
|
|
625
|
+
* them apart. The delta is cheap defensive verification against a future-adapter regression.
|
|
626
|
+
* @param adapter Adapter bound to `resource`.
|
|
627
|
+
* @param resource Resource to pull from.
|
|
628
|
+
* @param amount Underlying units to deliver to `to` (exact).
|
|
629
|
+
* @param to Recipient of the underlying.
|
|
630
|
+
* @return received Underlying units actually delivered to `to`, measured by balance delta.
|
|
631
|
+
*/
|
|
632
|
+
function _dispatchWithdraw(address adapter, address resource, uint256 amount, address to)
|
|
633
|
+
private
|
|
634
|
+
returns (uint256 received)
|
|
635
|
+
{
|
|
636
|
+
IERC20 assetToken = IERC20(_assetToken);
|
|
637
|
+
uint256 preBalTo = assetToken.balanceOf(to);
|
|
638
|
+
adapter.functionDelegateCall(abi.encodeCall(IResourceAdapter.withdraw, (resource, amount, to)));
|
|
639
|
+
received = assetToken.balanceOf(to) - preBalTo;
|
|
640
|
+
|
|
641
|
+
emit WithdrawRouted(resource, amount);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* @notice External self-call shell so the inner deposit cascade can `try/catch` a per-resource
|
|
646
|
+
* dispatch — a library delegatecall cannot be wrapped directly. Reverts unless the
|
|
647
|
+
* caller is this YieldGroup itself.
|
|
648
|
+
* @dev Runs the shared {_dispatchDeposit} body in external context. `address(this)` stays the
|
|
649
|
+
* YieldGroup, so the adapter's `onlyDelegateCall` check still passes. NOT `nonReentrant`
|
|
650
|
+
* (the cascade self-call must not trip the guard held by the public entrypoint) and NOT
|
|
651
|
+
* used by the strict reallocate path, which keeps calling `_dispatchDeposit` directly.
|
|
652
|
+
* @param adapter Adapter bound to `resource`.
|
|
653
|
+
* @param resource Resource to deposit into.
|
|
654
|
+
* @param amount Underlying units to place (exact).
|
|
655
|
+
* @return placed Underlying units placed (equals `amount` on success).
|
|
656
|
+
*/
|
|
657
|
+
// solhint-disable-next-line ordering
|
|
658
|
+
function dispatchDepositExternal(address adapter, address resource, uint256 amount)
|
|
659
|
+
external
|
|
660
|
+
returns (uint256 placed)
|
|
661
|
+
{
|
|
662
|
+
if (msg.sender != address(this)) revert OnlySelf();
|
|
663
|
+
_dispatchDeposit(adapter, resource, amount);
|
|
664
|
+
return amount;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* @notice External self-call shell so the inner withdraw cascade can `try/catch` a per-resource
|
|
669
|
+
* dispatch. Same guard and context guarantees as {dispatchDepositExternal}.
|
|
670
|
+
* @param adapter Adapter bound to `resource`.
|
|
671
|
+
* @param resource Resource to pull from.
|
|
672
|
+
* @param amount Underlying units to deliver to `to` (exact).
|
|
673
|
+
* @param to Recipient of the underlying.
|
|
674
|
+
* @return received Underlying units actually delivered (the caller asserts `received == amount`
|
|
675
|
+
* in its own frame; see {_tryWithdrawOneResource}).
|
|
676
|
+
*/
|
|
677
|
+
// solhint-disable-next-line ordering
|
|
678
|
+
function dispatchWithdrawExternal(address adapter, address resource, uint256 amount, address to)
|
|
679
|
+
external
|
|
680
|
+
returns (uint256 received)
|
|
681
|
+
{
|
|
682
|
+
if (msg.sender != address(this)) revert OnlySelf();
|
|
683
|
+
return _dispatchWithdraw(adapter, resource, amount, to);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* @notice Remove the first occurrence of `target` from `queue` via swap-and-pop.
|
|
688
|
+
* @param queue Storage array to mutate.
|
|
689
|
+
* @param target Address to remove (no-op if absent).
|
|
690
|
+
*/
|
|
691
|
+
function _removeFromQueue(address[] storage queue, address target) internal {
|
|
692
|
+
uint256 len = queue.length;
|
|
693
|
+
for (uint256 i; i < len; ++i) {
|
|
694
|
+
if (queue[i] == target) {
|
|
695
|
+
if (i != len - 1) {
|
|
696
|
+
queue[i] = queue[len - 1];
|
|
697
|
+
}
|
|
698
|
+
queue.pop();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ============================== Private — view ===========================
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* @notice Validate a routing queue: every entry registered, no duplicates.
|
|
708
|
+
* @param queue Candidate queue.
|
|
709
|
+
*/
|
|
710
|
+
function _validateQueue(address[] calldata queue) private view {
|
|
711
|
+
uint256 len = queue.length;
|
|
712
|
+
for (uint256 i; i < len; ++i) {
|
|
713
|
+
address resource = queue[i];
|
|
714
|
+
if (!_resourcesConfig[resource].registered) revert ResourceNotRegistered(resource);
|
|
715
|
+
for (uint256 j = i + 1; j < len; ++j) {
|
|
716
|
+
if (queue[j] == resource) revert InvalidQueue();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* @notice Revert if `queue` omits any registered resource that still holds receipt tokens.
|
|
723
|
+
* @param queue Candidate withdraw queue.
|
|
724
|
+
*/
|
|
725
|
+
function _requireWithdrawQueueCoversFundedResources(address[] calldata queue) private view {
|
|
726
|
+
address[] storage list = _resources;
|
|
727
|
+
uint256 len = list.length;
|
|
728
|
+
for (uint256 i; i < len; ++i) {
|
|
729
|
+
address resource = list[i];
|
|
730
|
+
uint256 held = IResourceAdapter(_resourcesConfig[resource].adapter).receiptBalance(resource, address(this));
|
|
731
|
+
if (held != 0 && !_queueContains(queue, resource)) {
|
|
732
|
+
revert WithdrawQueueOmitsFundedResource(resource);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* @notice Linear membership test over a calldata address array.
|
|
739
|
+
* @param queue Array to scan.
|
|
740
|
+
* @param target Address to find.
|
|
741
|
+
* @return found True iff `target` appears in `queue`.
|
|
742
|
+
*/
|
|
743
|
+
function _queueContains(address[] calldata queue, address target) private pure returns (bool found) {
|
|
744
|
+
uint256 len = queue.length;
|
|
745
|
+
for (uint256 i; i < len; ++i) {
|
|
746
|
+
if (queue[i] == target) return true;
|
|
747
|
+
}
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ============================== Internal — virtual hooks =================
|
|
752
|
+
// These capture the entire per-family divergence; each concrete overrides only what it needs.
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* @notice Advance a resource's internal state before a deposit/withdraw sizing read.
|
|
756
|
+
* @dev Base default: no-op. `YieldGroupFRV` overrides to call `IFRVVault.updateVaultState()`.
|
|
757
|
+
* Mutating, and invoked OUTSIDE the cascade try/catch so a poke failure halts the
|
|
758
|
+
* operation rather than being routed around.
|
|
759
|
+
*/
|
|
760
|
+
// solhint-disable-next-line no-empty-blocks
|
|
761
|
+
function _prepareResource(
|
|
762
|
+
address /* resource */
|
|
763
|
+
)
|
|
764
|
+
internal
|
|
765
|
+
virtual { }
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* @notice Per-resource minimum deposit floor.
|
|
769
|
+
* @dev Base default: `0` (no floor). `YieldGroupFRV` overrides to return the vault's
|
|
770
|
+
* `minSupplierDeposit`. A non-zero floor makes a sub-floor non-tail deposit skip (cascade)
|
|
771
|
+
* or revert (targeted) instead of failing inside the resource.
|
|
772
|
+
* @return minimum Minimum deposit in asset units (`0` = no floor).
|
|
773
|
+
*/
|
|
774
|
+
function _minDeposit(
|
|
775
|
+
address /* resource */
|
|
776
|
+
)
|
|
777
|
+
internal
|
|
778
|
+
view
|
|
779
|
+
virtual
|
|
780
|
+
returns (uint256 minimum)
|
|
781
|
+
{
|
|
782
|
+
return 0;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* @notice Spare deposit room for `resource` right now.
|
|
787
|
+
* @dev Base default: the resource's protocol-level headroom, unclamped. The generic
|
|
788
|
+
* `YieldGroup` overrides this to further clamp by its optional per-resource cap; lifecycle
|
|
789
|
+
* families (FRV) keep the raw headroom.
|
|
790
|
+
* @param resource Resource to size.
|
|
791
|
+
* @param adapter Adapter bound to `resource`.
|
|
792
|
+
* @return room Protocol-level deposit headroom for `resource`.
|
|
793
|
+
*/
|
|
794
|
+
function _resourceDepositRoom(address resource, address adapter) internal view virtual returns (uint256 room) {
|
|
795
|
+
return IResourceAdapter(adapter).maxDeposit(resource);
|
|
796
|
+
}
|
|
797
|
+
}
|