@venusprotocol/liquidity-hub 2.0.0 → 2.1.0-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/contracts/Hub/Hub.sol +2 -0
- package/contracts/YieldGroup/YieldGroupCentrifuge.sol +442 -0
- package/contracts/YieldGroup/base/NavGuard.sol +412 -0
- package/contracts/YieldGroup/base/YieldGroupBase.sol +24 -2
- package/contracts/adapters/AdapterCentrifuge.sol +358 -0
- package/contracts/interfaces/IAdapterCentrifuge.sol +64 -0
- package/contracts/interfaces/INavGuard.sol +339 -0
- package/contracts/interfaces/IYieldGroupCentrifuge.sol +248 -0
- package/contracts/interfaces/external/ICentrifugeAsyncRequestManager.sol +43 -0
- package/contracts/interfaces/external/ICentrifugeAsyncVault.sol +94 -0
- package/deployments/bscmainnet/AdapterCentrifuge.json +421 -0
- package/deployments/bscmainnet/CentrifugeBeacon.json +110 -0
- package/deployments/bscmainnet/CentrifugeSource_USDT.json +1708 -0
- package/deployments/bscmainnet/YieldGroupCentrifugeImpl.json +1708 -0
- package/deployments/bscmainnet_addresses.json +4 -0
- package/deployments/bsctestnet/AdapterCentrifuge.json +421 -0
- package/deployments/bsctestnet/CentrifugeBeacon.json +110 -0
- package/deployments/bsctestnet/CentrifugeSource_USDT.json +1708 -0
- package/deployments/bsctestnet/MockCentrifugeShare_USDT.json +472 -0
- package/deployments/bsctestnet/MockCentrifugeVault_USDT.json +1284 -0
- package/deployments/bsctestnet/YieldGroupCentrifugeImpl.json +1708 -0
- package/deployments/bsctestnet_addresses.json +6 -0
- package/package.json +3 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { INavGuard } from "../../interfaces/INavGuard.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title NavGuard
|
|
8
|
+
* @author Venus
|
|
9
|
+
* @notice Reusable per-resource band on a position's own value. Extend this when a position's
|
|
10
|
+
* value is reported by a counterparty that nothing else validates.
|
|
11
|
+
* @dev The band vouches for a value: the last counterparty figure we accepted, grown at `driftBps`
|
|
12
|
+
* a year, plus whatever we have moved in or out since. {_guardedNav} holds valuation a gap
|
|
13
|
+
* either side of that and never reverts. The gaps are a percentage of the counterparty's part
|
|
14
|
+
* only, so our own moves never widen them. Take everything out and the band closes; a deposit
|
|
15
|
+
* or {setNavGuardSnapshot} reopens it.
|
|
16
|
+
*
|
|
17
|
+
* Two stored numbers carry that, and {_currentBand} is the only place they are combined:
|
|
18
|
+
*
|
|
19
|
+
* - `anchor` is the counterparty's part: the last figure they reported that we accepted, less
|
|
20
|
+
* anything we have withdrawn beyond our own deposits. Both gaps are sized off it alone, and
|
|
21
|
+
* {_updateNavGuard} moves it once per `interval`.
|
|
22
|
+
* - `centre` is where the band sits: the anchor plus our own deposits since. {_recordInflow}
|
|
23
|
+
* and {_recordOutflow} move it.
|
|
24
|
+
*
|
|
25
|
+
* Two clocks, because the two things they time are unrelated. `anchoredAt` is the re-anchor
|
|
26
|
+
* clock and only a re-anchor moves it, or one dust deposit per interval would hold the band
|
|
27
|
+
* frozen. `driftFrom` is the drift baseline and every flow resets it, having first written the
|
|
28
|
+
* drift accrued so far into both numbers. That is what lets our deposits earn drift from when
|
|
29
|
+
* they arrived without earning any for the period before.
|
|
30
|
+
*
|
|
31
|
+
* Clamping down over-reports a real loss, so the floor rate-limits marking down rather than
|
|
32
|
+
* preventing it. Only {_guardedNav} reads `capEnabled` and `floorEnabled`; the re-anchor and
|
|
33
|
+
* both flow calls run whether or not a side is armed, so the band keeps tracking while off.
|
|
34
|
+
*
|
|
35
|
+
* Extenders supply {_observedNav} (must not revert; it reports whether it could read) and
|
|
36
|
+
* {_requireGuardConfig}, which gates all three governance setters.
|
|
37
|
+
*
|
|
38
|
+
* Storage: one mapping plus a gap, 20 slots total. Extenders are upgradeable proxies — do not
|
|
39
|
+
* change this contract's position in the inheritance chain.
|
|
40
|
+
*/
|
|
41
|
+
abstract contract NavGuard is INavGuard {
|
|
42
|
+
// ============================== Constants =================================
|
|
43
|
+
|
|
44
|
+
/// @dev Basis-point denominator for the drift rate and both gaps.
|
|
45
|
+
uint256 private constant BPS = 10_000;
|
|
46
|
+
|
|
47
|
+
/// @dev One year, for annualizing `driftBps`.
|
|
48
|
+
uint256 private constant SECONDS_PER_YEAR = 365 days;
|
|
49
|
+
|
|
50
|
+
// ============================== State =====================================
|
|
51
|
+
|
|
52
|
+
/// @notice Optional value band for each resource. Never configured when `interval == 0`.
|
|
53
|
+
/// @dev The extender's removal paths must clear this with {_clearNavGuard}, or a re-added
|
|
54
|
+
/// resource inherits the old band and {setNavGuardRate} holds its new anchor inside it.
|
|
55
|
+
mapping(address resource => NavBand band) internal _navGuard;
|
|
56
|
+
|
|
57
|
+
/// @dev Reserved slots for future fields. Shrink this when adding one.
|
|
58
|
+
uint256[19] private __gap;
|
|
59
|
+
|
|
60
|
+
// ============================== External — mutating ========================
|
|
61
|
+
|
|
62
|
+
/// @inheritdoc INavGuard
|
|
63
|
+
/// @dev The ACM role each of these three setters checks is its own signature, verbatim. So
|
|
64
|
+
/// they are also the three strings an onboarding VIP has to grant.
|
|
65
|
+
function setNavGuardRate(
|
|
66
|
+
address resource,
|
|
67
|
+
uint16 driftBps,
|
|
68
|
+
uint16 upGapBps,
|
|
69
|
+
uint16 downGapBps,
|
|
70
|
+
uint32 interval,
|
|
71
|
+
bool capEnabled,
|
|
72
|
+
bool floorEnabled
|
|
73
|
+
) external virtual override {
|
|
74
|
+
_requireGuardConfig("setNavGuardRate(address,uint16,uint16,uint16,uint32,bool,bool)", resource);
|
|
75
|
+
_setNavGuardRate(resource, driftBps, upGapBps, downGapBps, interval, capEnabled, floorEnabled);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// @inheritdoc INavGuard
|
|
79
|
+
function setNavGuardSnapshot(address resource, uint128 snapshot, uint64 timestamp) external virtual override {
|
|
80
|
+
_requireGuardConfig("setNavGuardSnapshot(address,uint128,uint64)", resource);
|
|
81
|
+
_setNavGuardSnapshot(resource, snapshot, timestamp);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// @inheritdoc INavGuard
|
|
85
|
+
function setNavGuardEnabled(address resource, bool capEnabled, bool floorEnabled) external virtual override {
|
|
86
|
+
_requireGuardConfig("setNavGuardEnabled(address,bool,bool)", resource);
|
|
87
|
+
_setNavGuardEnabled(resource, capEnabled, floorEnabled);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================== External — view ============================
|
|
91
|
+
|
|
92
|
+
/// @inheritdoc INavGuard
|
|
93
|
+
function navGuard(address resource) external view override returns (NavBand memory) {
|
|
94
|
+
return _navGuard[resource];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// @inheritdoc INavGuard
|
|
98
|
+
function navGuardStatus(address resource)
|
|
99
|
+
external
|
|
100
|
+
view
|
|
101
|
+
override
|
|
102
|
+
returns (
|
|
103
|
+
uint256 observedValue,
|
|
104
|
+
uint256 minAllowedValue,
|
|
105
|
+
uint256 maxAllowedValue,
|
|
106
|
+
bool isClamped,
|
|
107
|
+
uint256 clampedValue
|
|
108
|
+
)
|
|
109
|
+
{
|
|
110
|
+
NavBand storage band = _navGuard[resource];
|
|
111
|
+
if (band.interval == 0) return (0, 0, 0, false, 0);
|
|
112
|
+
|
|
113
|
+
(observedValue,) = _observedNav(resource);
|
|
114
|
+
|
|
115
|
+
// Both bounds are returned even when their side is switched off, so monitoring can see
|
|
116
|
+
// where an unarmed side would bind if it were armed.
|
|
117
|
+
(, minAllowedValue, maxAllowedValue) = _currentBand(band);
|
|
118
|
+
|
|
119
|
+
// `_guardedNav` does the clamping, so this only checks whether it clamped or returned `observedValue`.
|
|
120
|
+
uint256 guarded = _guardedNav(resource, observedValue);
|
|
121
|
+
if (guarded == observedValue) return (observedValue, minAllowedValue, maxAllowedValue, false, 0);
|
|
122
|
+
return (observedValue, minAllowedValue, maxAllowedValue, true, guarded);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================== Internal — configuration ==================
|
|
126
|
+
|
|
127
|
+
/// @dev Validation and storage behind {setNavGuardRate}; auth and registration have already
|
|
128
|
+
/// run in {_requireGuardConfig}.
|
|
129
|
+
function _setNavGuardRate(
|
|
130
|
+
address resource,
|
|
131
|
+
uint16 driftBps,
|
|
132
|
+
uint16 upGapBps,
|
|
133
|
+
uint16 downGapBps,
|
|
134
|
+
uint32 interval,
|
|
135
|
+
bool capEnabled,
|
|
136
|
+
bool floorEnabled
|
|
137
|
+
) internal {
|
|
138
|
+
// A stored zero interval would read as "no band" in every other path here, and both gaps
|
|
139
|
+
// stay under 100% so `_currentBand`'s floor subtraction cannot underflow.
|
|
140
|
+
if (interval == 0 || upGapBps >= BPS || downGapBps >= BPS) revert InvalidNavGuardRate();
|
|
141
|
+
|
|
142
|
+
// Arming on a failed read would centre the band at zero and value a live position at nothing.
|
|
143
|
+
(uint256 anchor, bool readable) = _observedNav(resource);
|
|
144
|
+
if (!readable) revert NavGuardUnreadable(resource);
|
|
145
|
+
|
|
146
|
+
// A retune re-anchors on what the vault reports now, held inside the band already in force,
|
|
147
|
+
// so a gap change cannot adopt a value the old band was refusing. A zero reading skips that
|
|
148
|
+
// hold, or `_clamp` would raise it to the old floor and centre the new band on value that is gone.
|
|
149
|
+
NavBand storage current = _navGuard[resource];
|
|
150
|
+
if (current.interval != 0 && anchor != 0) {
|
|
151
|
+
(, uint256 floor, uint256 cap) = _currentBand(current);
|
|
152
|
+
anchor = _clamp(anchor, floor, cap);
|
|
153
|
+
}
|
|
154
|
+
if (anchor > type(uint128).max) revert InvalidNavGuardRate();
|
|
155
|
+
|
|
156
|
+
_navGuard[resource] = NavBand({
|
|
157
|
+
anchor: uint128(anchor),
|
|
158
|
+
centre: uint128(anchor),
|
|
159
|
+
anchoredAt: uint64(block.timestamp),
|
|
160
|
+
driftFrom: uint64(block.timestamp),
|
|
161
|
+
interval: interval,
|
|
162
|
+
driftBps: driftBps,
|
|
163
|
+
upGapBps: upGapBps,
|
|
164
|
+
downGapBps: downGapBps,
|
|
165
|
+
capEnabled: capEnabled,
|
|
166
|
+
floorEnabled: floorEnabled
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
emit NavGuardConfigured(
|
|
170
|
+
resource, uint128(anchor), driftBps, upGapBps, downGapBps, interval, capEnabled, floorEnabled
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// @dev Validation and storage behind {setNavGuardSnapshot}; auth and registration have
|
|
175
|
+
/// already run.
|
|
176
|
+
function _setNavGuardSnapshot(address resource, uint128 snapshot, uint64 timestamp) internal {
|
|
177
|
+
NavBand storage band = _navGuard[resource];
|
|
178
|
+
if (band.interval == 0) revert InvalidNavGuardSnapshot();
|
|
179
|
+
if (snapshot == 0) revert InvalidNavGuardSnapshot();
|
|
180
|
+
if (timestamp == 0 || timestamp > block.timestamp) revert InvalidNavGuardSnapshot();
|
|
181
|
+
|
|
182
|
+
band.anchor = snapshot;
|
|
183
|
+
band.centre = snapshot;
|
|
184
|
+
band.anchoredAt = timestamp;
|
|
185
|
+
band.driftFrom = timestamp;
|
|
186
|
+
|
|
187
|
+
emit NavGuardReanchored(resource, snapshot, timestamp);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// @dev Validation and storage behind {setNavGuardEnabled}; auth and registration have already
|
|
191
|
+
/// run.
|
|
192
|
+
function _setNavGuardEnabled(address resource, bool capEnabled, bool floorEnabled) internal {
|
|
193
|
+
NavBand storage band = _navGuard[resource];
|
|
194
|
+
if ((capEnabled || floorEnabled) && band.interval == 0) revert NavGuardNotConfigured(resource);
|
|
195
|
+
if (band.capEnabled == capEnabled && band.floorEnabled == floorEnabled) return;
|
|
196
|
+
|
|
197
|
+
band.capEnabled = capEnabled;
|
|
198
|
+
band.floorEnabled = floorEnabled;
|
|
199
|
+
emit NavGuardEnabledSet(resource, capEnabled, floorEnabled);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// @dev Clears the band for `resource`. Used by the extender's write-off / removal paths.
|
|
203
|
+
function _clearNavGuard(address resource) internal {
|
|
204
|
+
delete _navGuard[resource];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================== Internal — accrual =========================
|
|
208
|
+
|
|
209
|
+
/// @dev Must not revert: this runs inside accrual. Every failure to read returns early and
|
|
210
|
+
/// leaves the band where it was.
|
|
211
|
+
///
|
|
212
|
+
/// The clamp log lives here because {_guardedNav} is a view and cannot emit. It comes after
|
|
213
|
+
/// the re-anchor, which moves the band, and `Hub._accrueFees` reads NAV later in the same
|
|
214
|
+
/// call. Logging any earlier would name a bound valuation no longer uses.
|
|
215
|
+
function _updateNavGuard(address resource) internal {
|
|
216
|
+
NavBand storage band = _navGuard[resource];
|
|
217
|
+
if (band.interval == 0) return;
|
|
218
|
+
|
|
219
|
+
// A failed read is no information, and a zero the counterparty publishes must not close the
|
|
220
|
+
// band: only our own withdrawals do that, through `_recordOutflow`.
|
|
221
|
+
(uint256 observed, bool readable) = _observedNav(resource);
|
|
222
|
+
if (!readable || observed == 0) return;
|
|
223
|
+
|
|
224
|
+
if (block.timestamp - band.anchoredAt >= band.interval) _reanchor(resource, band, observed);
|
|
225
|
+
|
|
226
|
+
uint256 guarded = _guardedNav(resource, observed);
|
|
227
|
+
if (guarded != observed) emit NavGuardClamped(resource, observed, guarded);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// @dev Once per `interval` the band moves onto what the counterparty reports now, so real
|
|
231
|
+
/// growth does not eventually push a healthy position through the cap. `observed` is held
|
|
232
|
+
/// inside the band in force before it is stored, which is what stops a single bad reading
|
|
233
|
+
/// being adopted whole: 500 against a cap of 102 stores 102, the next band is built around
|
|
234
|
+
/// 102, and valuation can climb only one gap per interval.
|
|
235
|
+
function _reanchor(address resource, NavBand storage band, uint256 observed) private {
|
|
236
|
+
(uint256 centre, uint256 floor, uint256 cap) = _currentBand(band);
|
|
237
|
+
if (centre == 0) return;
|
|
238
|
+
|
|
239
|
+
uint256 anchor = _clamp(observed, floor, cap);
|
|
240
|
+
if (anchor > type(uint128).max) return;
|
|
241
|
+
|
|
242
|
+
band.anchor = uint128(anchor);
|
|
243
|
+
band.centre = uint128(anchor);
|
|
244
|
+
band.anchoredAt = uint64(block.timestamp);
|
|
245
|
+
band.driftFrom = uint64(block.timestamp);
|
|
246
|
+
emit NavGuardReanchored(resource, uint128(anchor), uint64(block.timestamp));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================== Internal — our own moves ===================
|
|
250
|
+
|
|
251
|
+
/// @dev Moves the centre up by what we put into `resource`. No part of the deposit reaches the
|
|
252
|
+
/// anchor, or it would buy itself a gap on the value it just added, and `anchoredAt` stays
|
|
253
|
+
/// where it was, or one dust deposit per interval would hold the band frozen for good. The
|
|
254
|
+
/// anchor does take the drift {_settleDrift} writes in first, which is growth it had already
|
|
255
|
+
/// earned.
|
|
256
|
+
///
|
|
257
|
+
/// A position now worth exactly what we sent was empty a moment before, so whatever the band
|
|
258
|
+
/// still had stored is value that left without {_recordOutflow} being told, by a sweep or a
|
|
259
|
+
/// write-off. The read is what catches that: left in place, a centre of 48 plus a deposit of
|
|
260
|
+
/// 10 would report 58 over a position holding 10.
|
|
261
|
+
///
|
|
262
|
+
/// Reverts {NavGuardCentreOverflow} past `uint128`. Reverting unwinds the deposit instead
|
|
263
|
+
/// of routing value the band cannot record.
|
|
264
|
+
function _recordInflow(address resource, uint256 amount) internal {
|
|
265
|
+
NavBand storage band = _navGuard[resource];
|
|
266
|
+
if (band.interval == 0 || amount == 0) return;
|
|
267
|
+
|
|
268
|
+
uint256 centre = _settleDrift(band) + amount;
|
|
269
|
+
if (centre > type(uint128).max) revert NavGuardCentreOverflow(resource, centre);
|
|
270
|
+
|
|
271
|
+
(uint256 value, bool readable) = _observedNav(resource);
|
|
272
|
+
if (readable && value == amount && centre != amount) {
|
|
273
|
+
emit NavGuardClosed(resource, band.centre, 0);
|
|
274
|
+
band.anchor = 0;
|
|
275
|
+
centre = amount;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
band.centre = uint128(centre);
|
|
279
|
+
emit NavGuardCentreMoved(resource, band.anchor, uint128(centre));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// @dev Moves the centre down by what left `resource`, or closes the band. A withdrawal comes out
|
|
283
|
+
/// of our own deposits first, so once the centre falls below the anchor the anchor comes
|
|
284
|
+
/// down with it. Left high, the gaps would go on being sized to money that has gone.
|
|
285
|
+
///
|
|
286
|
+
/// The centre it is measured against is the one {_settleDrift} has just brought up to date,
|
|
287
|
+
/// so a payout is compared with what the band vouches for now rather than with a figure
|
|
288
|
+
/// that stopped at the last re-anchor.
|
|
289
|
+
///
|
|
290
|
+
/// The band closes when the withdrawal reaches the centre, or when the position reads empty
|
|
291
|
+
/// afterwards. The amount alone cannot show the second while the floor holds the band above
|
|
292
|
+
/// a loss: take 50 out of a position worth 50 that the band reports at 98 and the centre
|
|
293
|
+
/// would be left at 48 over nothing, for the next deposit to be reported 48 above what went
|
|
294
|
+
/// in. An unreadable read does not close the band, since an empty position needs no price.
|
|
295
|
+
function _recordOutflow(address resource, uint256 amount) internal {
|
|
296
|
+
NavBand storage band = _navGuard[resource];
|
|
297
|
+
if (band.interval == 0 || amount == 0 || band.centre == 0) return;
|
|
298
|
+
|
|
299
|
+
uint256 centre = _settleDrift(band);
|
|
300
|
+
(uint256 left, bool readable) = _observedNav(resource);
|
|
301
|
+
if (amount >= centre || (readable && left == 0)) {
|
|
302
|
+
band.anchor = 0;
|
|
303
|
+
band.centre = 0;
|
|
304
|
+
emit NavGuardClosed(resource, centre, amount);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
centre -= amount;
|
|
309
|
+
band.centre = uint128(centre);
|
|
310
|
+
if (band.anchor > centre) band.anchor = uint128(centre);
|
|
311
|
+
emit NavGuardCentreMoved(resource, band.anchor, uint128(centre));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================== Internal — view ============================
|
|
315
|
+
|
|
316
|
+
/// @dev Revert unless the caller may call `signature` and `resource` is one the extender manages.
|
|
317
|
+
/// Check the caller first, or an unauthorized one learns what this extender has registered.
|
|
318
|
+
///
|
|
319
|
+
/// One hook for both, so a function added later cannot run the first check and forget the
|
|
320
|
+
/// second. It is abstract because the extender already holds an access-control manager.
|
|
321
|
+
function _requireGuardConfig(string memory signature, address resource) internal view virtual;
|
|
322
|
+
|
|
323
|
+
/// @dev What `resource` is worth to the extender right now, in asset units and unclamped.
|
|
324
|
+
/// Must not revert: answer `(0, false)` for a read that failed.
|
|
325
|
+
function _observedNav(address resource) internal view virtual returns (uint256 value, bool readable);
|
|
326
|
+
|
|
327
|
+
/// @dev The band applied to `observedNav`, on whichever sides are armed: the floor below it,
|
|
328
|
+
/// the cap above it, `observedNav` itself in between. Never reverts.
|
|
329
|
+
///
|
|
330
|
+
/// A zero `observedNav` passes straight through rather than being clamped up to the
|
|
331
|
+
/// floor: that would report value for a resource that demonstrably has none, and a
|
|
332
|
+
/// resource can only be removed once it is empty.
|
|
333
|
+
function _guardedNav(address resource, uint256 observedNav) internal view returns (uint256 value) {
|
|
334
|
+
if (observedNav == 0) return 0;
|
|
335
|
+
|
|
336
|
+
NavBand storage band = _navGuard[resource];
|
|
337
|
+
if (band.interval == 0) return observedNav;
|
|
338
|
+
|
|
339
|
+
bool capOn = band.capEnabled;
|
|
340
|
+
bool floorOn = band.floorEnabled;
|
|
341
|
+
if (!capOn && !floorOn) return observedNav;
|
|
342
|
+
|
|
343
|
+
(, uint256 floor, uint256 cap) = _currentBand(band);
|
|
344
|
+
if (floorOn && observedNav < floor) return floor;
|
|
345
|
+
if (capOn && observedNav > cap) return cap;
|
|
346
|
+
return observedNav;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// @dev The band right now. Both numbers grow at `driftBps` since `driftFrom`, because
|
|
350
|
+
/// Centrifuge prices the whole position with one share price and our own deposits sit in
|
|
351
|
+
/// the same fund as the counterparty's part. Crediting drift to the anchor alone left a
|
|
352
|
+
/// deposit with no growth allowance until the next re-anchor: 100 armed, 400 deposited and
|
|
353
|
+
/// thirty days at exactly the configured 8% put the cap at 502.67 over a position worth
|
|
354
|
+
/// 503.29, so an honest fund was clamped.
|
|
355
|
+
///
|
|
356
|
+
/// The gaps are sized to the anchor, not to the centre, and that asymmetry is the point. A
|
|
357
|
+
/// deposit raises only the centre, so it buys no headroom on the value it just added. A
|
|
358
|
+
/// withdrawal past our own deposits lowers the anchor with the centre, so the gaps resize
|
|
359
|
+
/// to what is left: take 99 out of 100 with a 2% gap and the cap is 1.02, where an anchor
|
|
360
|
+
/// left at 100 would have allowed 3.
|
|
361
|
+
///
|
|
362
|
+
/// Nothing here can underflow. The anchor never exceeds the centre and {_grown} is
|
|
363
|
+
/// monotonic, so it does not after drifting either; both gaps are held under 100% at
|
|
364
|
+
/// configuration; and `driftFrom` is never in the future because every writer uses
|
|
365
|
+
/// `block.timestamp` or rejects a later stamp. A closed band returns zeros.
|
|
366
|
+
function _currentBand(NavBand storage band) internal view returns (uint256 centre, uint256 floor, uint256 cap) {
|
|
367
|
+
uint256 elapsed = block.timestamp - band.driftFrom;
|
|
368
|
+
uint256 driftBps = band.driftBps;
|
|
369
|
+
uint256 anchor = _grown(band.anchor, driftBps, elapsed);
|
|
370
|
+
centre = _grown(band.centre, driftBps, elapsed);
|
|
371
|
+
|
|
372
|
+
floor = centre - (anchor * band.downGapBps) / BPS;
|
|
373
|
+
cap = centre + (anchor * band.upGapBps) / BPS;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// @dev `value` after `elapsed` seconds at `driftBps` a year, capped where it would stop fitting
|
|
377
|
+
/// the slot it is stored in. The cap is unreachable in practice: it needs a position near
|
|
378
|
+
/// `uint128` that has also gone a decade without a re-anchor. Capping rather than reverting
|
|
379
|
+
/// keeps every caller non-reverting, and it can only lower the band.
|
|
380
|
+
function _grown(uint256 value, uint256 driftBps, uint256 elapsed) private pure returns (uint256) {
|
|
381
|
+
uint256 grown = value + (value * driftBps * elapsed) / (BPS * SECONDS_PER_YEAR);
|
|
382
|
+
return grown > type(uint128).max ? type(uint128).max : grown;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// @dev Writes the drift accrued since `driftFrom` into both stored numbers and restarts that
|
|
386
|
+
/// clock, then returns the centre. Every flow calls this before moving anything, so the
|
|
387
|
+
/// flow is measured against the band as it stands at that moment and money arriving now
|
|
388
|
+
/// earns drift only from now. Without it, growing the centre would hand a deposit the drift
|
|
389
|
+
/// of the whole period since the last re-anchor, which is headroom bought with deposit size.
|
|
390
|
+
///
|
|
391
|
+
/// `anchoredAt` is untouched. The re-anchor clock belongs to the counterparty, and letting
|
|
392
|
+
/// a flow move it would let one dust deposit per interval hold the band frozen.
|
|
393
|
+
function _settleDrift(NavBand storage band) private returns (uint256 centre) {
|
|
394
|
+
centre = band.centre;
|
|
395
|
+
uint256 elapsed = block.timestamp - band.driftFrom;
|
|
396
|
+
uint256 driftBps = band.driftBps;
|
|
397
|
+
|
|
398
|
+
if (elapsed != 0 && driftBps != 0 && centre != 0) {
|
|
399
|
+
centre = _grown(centre, driftBps, elapsed);
|
|
400
|
+
band.centre = uint128(centre);
|
|
401
|
+
band.anchor = uint128(_grown(band.anchor, driftBps, elapsed));
|
|
402
|
+
}
|
|
403
|
+
band.driftFrom = uint64(block.timestamp);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// @dev `value` held inside `[floor, cap]`.
|
|
407
|
+
function _clamp(uint256 value, uint256 floor, uint256 cap) private pure returns (uint256) {
|
|
408
|
+
if (value < floor) return floor;
|
|
409
|
+
if (value > cap) return cap;
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -164,6 +164,7 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
164
164
|
// setter is registered-only. Removal restores a clean slate.
|
|
165
165
|
delete _resourceCap[resource];
|
|
166
166
|
delete _resourcesConfig[resource];
|
|
167
|
+
_onResourceRemoved(resource);
|
|
167
168
|
|
|
168
169
|
emit ResourceRemoved(resource);
|
|
169
170
|
}
|
|
@@ -604,11 +605,27 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
604
605
|
* Callers own the registration / pause / capacity checks and must ensure this
|
|
605
606
|
* YieldGroup already holds `amount` of underlying. Delegatecall credits receipt
|
|
606
607
|
* tokens to this YieldGroup; `Address.functionDelegateCall` bubbles adapter reverts.
|
|
608
|
+
*
|
|
609
|
+
* `virtual` so a family that has to account for its own value moves can hook here: this
|
|
610
|
+
* and {_dispatchWithdraw} are the only two places value enters or leaves a resource.
|
|
607
611
|
* @param adapter Adapter bound to `resource`.
|
|
608
612
|
* @param resource Resource to deposit into.
|
|
609
613
|
* @param amount Underlying units to place (exact).
|
|
610
614
|
*/
|
|
611
|
-
|
|
615
|
+
/**
|
|
616
|
+
* @notice Clear whatever per-resource state a family keeps outside this contract, called by
|
|
617
|
+
* {removeResource} once the resource is out of the registry and both queues.
|
|
618
|
+
* @dev Same reason {removeResource} clears `_resourceCap`: `addResource` never writes these,
|
|
619
|
+
* and the setters are registered-only, so anything left behind is silently inherited by a
|
|
620
|
+
* later re-`addResource` of the same address. A family with no extra state leaves this
|
|
621
|
+
* empty. Must not revert, or removal becomes unreachable.
|
|
622
|
+
* @param resource Resource being removed.
|
|
623
|
+
*/
|
|
624
|
+
// solhint-disable-next-line no-empty-blocks
|
|
625
|
+
function _onResourceRemoved(address resource) internal virtual { }
|
|
626
|
+
|
|
627
|
+
/// @custom:oz-upgrades-unsafe-allow-reachable delegatecall
|
|
628
|
+
function _dispatchDeposit(address adapter, address resource, uint256 amount) internal virtual {
|
|
612
629
|
adapter.functionDelegateCall(abi.encodeCall(IResourceAdapter.deposit, (resource, amount)));
|
|
613
630
|
emit DepositRouted(resource, amount);
|
|
614
631
|
}
|
|
@@ -623,14 +640,19 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
623
640
|
* a shortfall itself — doing so inside the cascade's external self-call frame would blur
|
|
624
641
|
* our own invariant with an external-protocol revert and force selector-matching to tell
|
|
625
642
|
* them apart. The delta is cheap defensive verification against a future-adapter regression.
|
|
643
|
+
*
|
|
644
|
+
* `virtual` for the reason on {_dispatchDeposit}. An override should use `received`, not
|
|
645
|
+
* `amount`.
|
|
626
646
|
* @param adapter Adapter bound to `resource`.
|
|
627
647
|
* @param resource Resource to pull from.
|
|
628
648
|
* @param amount Underlying units to deliver to `to` (exact).
|
|
629
649
|
* @param to Recipient of the underlying.
|
|
630
650
|
* @return received Underlying units actually delivered to `to`, measured by balance delta.
|
|
651
|
+
* @custom:oz-upgrades-unsafe-allow-reachable delegatecall
|
|
631
652
|
*/
|
|
632
653
|
function _dispatchWithdraw(address adapter, address resource, uint256 amount, address to)
|
|
633
|
-
|
|
654
|
+
internal
|
|
655
|
+
virtual
|
|
634
656
|
returns (uint256 received)
|
|
635
657
|
{
|
|
636
658
|
IERC20 assetToken = IERC20(_assetToken);
|