@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.
@@ -0,0 +1,339 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ /**
5
+ * @title INavGuard
6
+ * @author Venus
7
+ * @notice External interface for `NavGuard` — a per-resource band on a position's own value, for
8
+ * positions whose value is reported by a counterparty nothing else validates.
9
+ * @dev The band vouches for a value: the last counterparty figure we accepted, grown at the
10
+ * expected rate, plus whatever we have moved in or out since. Valuation is held a gap either
11
+ * side of that, `min(max(observed, floor), cap)`, and never reverts. The gaps are a percentage
12
+ * of the counterparty's part only, so our own moves never widen them. Take everything out and
13
+ * the band closes; a deposit or governance reopens it.
14
+ *
15
+ * It bounds value, not price: a position marked as `quantity x price` can be inflated
16
+ * through either factor. Value is the product plus whatever has no price at all, so one band
17
+ * covers every way the number can be wrong.
18
+ *
19
+ * Values are in the asset's own units, never a reference currency. The band therefore says
20
+ * nothing about what the asset is worth elsewhere: a depeg is invisible to it by design.
21
+ *
22
+ * Two things move the band, and they are deliberately different:
23
+ *
24
+ * - The periodic re-anchor, at most once per `interval`: the anchor adopts
25
+ * `clamp(observed, floor, cap)`. Clamped first, so a wrong value moves the band by one gap
26
+ * per interval and never becomes the baseline the next reading is judged against.
27
+ * - Our own moves: a deposit raises the centre by exactly the amount, and a withdrawal lowers
28
+ * it, taking the anchor down with it once our own deposits are used up. Neither reads a
29
+ * value that could raise the band, and neither touches the clock, so a deposit or
30
+ * withdrawal buys no gap of its own.
31
+ *
32
+ * Everything here is implemented once in `NavGuard`. The extending contract supplies the
33
+ * value source, the access-control hook, and the calls that record its own moves.
34
+ */
35
+ interface INavGuard {
36
+ // -------------------------------- Structs ---------------------------------
37
+
38
+ /**
39
+ * @notice One resource's value band. All zero until {setNavGuardRate} is called.
40
+ * @dev A side binds on valuation only while its own flag is true and `interval` is non-zero.
41
+ * They answer different questions: `interval == 0` means never configured, while a false
42
+ * flag means configured and that side switched off, keeping the band so it can be re-armed
43
+ * without re-supplying anything. Both flags false leaves the band tracking but clamping
44
+ * nothing.
45
+ *
46
+ * `anchor` and `centre` are stored as of `driftFrom`, and each grows at `driftBps` from
47
+ * there when read. A closed band has both at zero: every armed side then values the
48
+ * position at nothing, and only a deposit or {setNavGuardSnapshot} reopens it.
49
+ * @param anchor The counterparty's part of the position: the value accepted at the last
50
+ * re-anchor, or forced by governance, less what withdrawals have since taken past our own
51
+ * deposits. Never above `centre`. Both gaps are sized to it.
52
+ * @param centre `anchor` plus our own deposits since. What valuation is held around.
53
+ * @param anchoredAt When `anchor` was last set. `interval` is measured from here, and only a
54
+ * re-anchor moves it, so our own flows can never delay or bring one forward.
55
+ * @param driftFrom When both stored numbers were last brought up to date. Drift accrues from
56
+ * here, and every flow writes what has accrued into them and resets it, so money we add
57
+ * drifts from when it arrived rather than from the last re-anchor.
58
+ * @param interval Minimum seconds between periodic re-anchors. `0` means never configured.
59
+ * @param driftBps Annual drift of both stored numbers, in basis points. May be zero — a band
60
+ * that does not drift is the strictest valid setting.
61
+ * @param upGapBps Half-width above the centre, in basis points. Below 10_000.
62
+ * @param downGapBps Half-width below the centre, in basis points. Below 10_000.
63
+ * @param capEnabled Whether valuation is clamped down to the cap.
64
+ * @param floorEnabled Whether valuation is clamped up to the floor.
65
+ */
66
+ struct NavBand {
67
+ uint128 anchor;
68
+ uint128 centre;
69
+ uint64 anchoredAt;
70
+ uint64 driftFrom;
71
+ uint32 interval;
72
+ uint16 driftBps;
73
+ uint16 upGapBps;
74
+ uint16 downGapBps;
75
+ bool capEnabled;
76
+ bool floorEnabled;
77
+ }
78
+
79
+ // -------------------------------- Events ----------------------------------
80
+
81
+ /**
82
+ * @notice A resource's band was configured, anchored on the value observed in this transaction
83
+ * and held inside the band that was already in force, if any.
84
+ * @param resource Resource the band applies to.
85
+ * @param anchor Value the band is now anchored on. Zero means it is closed.
86
+ * @param driftBps Annual drift of the anchor, in basis points.
87
+ * @param upGapBps Half-width above the centre, in basis points.
88
+ * @param downGapBps Half-width below the centre, in basis points.
89
+ * @param interval Minimum seconds between periodic re-anchors.
90
+ * @param capEnabled Whether valuation is now clamped down to the cap.
91
+ * @param floorEnabled Whether valuation is now clamped up to the floor.
92
+ */
93
+ event NavGuardConfigured(
94
+ address indexed resource,
95
+ uint128 anchor,
96
+ uint16 driftBps,
97
+ uint16 upGapBps,
98
+ uint16 downGapBps,
99
+ uint32 interval,
100
+ bool capEnabled,
101
+ bool floorEnabled
102
+ );
103
+
104
+ /**
105
+ * @notice A resource's band had one or both sides switched on or off, without changing its
106
+ * configuration.
107
+ * @dev A side that is off reports whatever the counterparty says on that side, but the centre
108
+ * keeps tracking — see {setNavGuardEnabled}. Not the only event that carries the flags:
109
+ * {setNavGuardRate} takes them too, and reports them on {NavGuardConfigured}.
110
+ * @param resource Resource whose band was toggled.
111
+ * @param capEnabled Whether valuation is now clamped down to the cap.
112
+ * @param floorEnabled Whether valuation is now clamped up to the floor.
113
+ */
114
+ event NavGuardEnabledSet(address indexed resource, bool capEnabled, bool floorEnabled);
115
+
116
+ /**
117
+ * @notice A resource's anchor moved: the periodic re-anchor adopted a clamped reading, or
118
+ * governance forced one through {setNavGuardSnapshot}. The centre moves onto it too.
119
+ * @param resource Resource whose anchor moved.
120
+ * @param anchor New anchor, and new centre.
121
+ * @param anchoredAt Drift baseline and re-anchor clock from here on.
122
+ */
123
+ event NavGuardReanchored(address indexed resource, uint128 anchor, uint64 anchoredAt);
124
+
125
+ /**
126
+ * @notice We moved value into or out of a resource, and its centre moved by exactly that. The
127
+ * clock is untouched, and the anchor moves only when a withdrawal reaches into the
128
+ * counterparty's part.
129
+ * @param resource Resource whose centre moved.
130
+ * @param anchor The counterparty's part after the move. Zero right after a deposit means the
131
+ * deposit opened the band, and the band has no width until its first re-anchor.
132
+ * @param centre New stored centre, before drift.
133
+ */
134
+ event NavGuardCentreMoved(address indexed resource, uint128 anchor, uint128 centre);
135
+
136
+ /**
137
+ * @notice The band closed: both bounds are zero, and every armed side values the position at
138
+ * nothing until a deposit or {setNavGuardSnapshot} reopens it. A withdrawal took out at
139
+ * least what the centre held, or left the position empty, or a deposit found the
140
+ * position already empty under a centre nothing had told the band about.
141
+ * @dev Alert on this and read {navGuardStatus}. A nonzero `observedValue` there means the
142
+ * position still holds something that valuation is not counting. A zero `amount` is the
143
+ * deposit case, and says value left the position without the band being told, which a
144
+ * sweep does; the deposit that found it has reopened the band at exactly its own size.
145
+ * @param resource Resource whose band closed.
146
+ * @param centre Centre the band held before.
147
+ * @param amount Value that left the resource in this transaction; zero when a deposit found
148
+ * the position already empty.
149
+ */
150
+ event NavGuardClosed(address indexed resource, uint256 centre, uint256 amount);
151
+
152
+ /**
153
+ * @notice A resource's value was held to one of its band's bounds, and valuation reports that
154
+ * bound instead.
155
+ * @dev Fires from the Hub's accrual, so silence means nobody has transacted, not that nothing
156
+ * is clamped. {navGuardStatus} answers that on demand.
157
+ * @param resource Resource whose value was held to a bound.
158
+ * @param observedValue What the counterparty reports the position is worth.
159
+ * @param clampedValue What valuation reports instead. Below `observedValue` the cap bound,
160
+ * above it the floor.
161
+ */
162
+ event NavGuardClamped(address indexed resource, uint256 observedValue, uint256 clampedValue);
163
+
164
+ // -------------------------------- Errors ----------------------------------
165
+
166
+ /// @notice `setNavGuardRate()` got a zero `interval`, or an `upGapBps` or `downGapBps` of 100%
167
+ /// or more. A gap of zero is accepted, and is the tightest band on its side; switching a
168
+ /// side off is {setNavGuardEnabled}'s job.
169
+ error InvalidNavGuardRate();
170
+
171
+ /// @notice `setNavGuardRate()` could not read what the resource is worth. Arming on that would
172
+ /// centre the band at zero and value a live position at nothing, so the call fails.
173
+ /// Whatever stops the read has to be cleared first — for a Centrifuge vault that is
174
+ /// usually a share price the publisher has not set.
175
+ error NavGuardUnreadable(address resource);
176
+
177
+ /// @notice `setNavGuardSnapshot()` got a zero `snapshot`, a zero `timestamp`, a `timestamp` in
178
+ /// the future, or a resource whose band was never configured.
179
+ error InvalidNavGuardSnapshot();
180
+
181
+ /// @notice `setNavGuardEnabled()` was asked to arm a side of a band that was never configured.
182
+ /// There is nothing to clamp to until {setNavGuardRate} has run.
183
+ error NavGuardNotConfigured(address resource);
184
+
185
+ /// @notice A deposit would have pushed a band's centre past `uint128`, so the deposit reverted.
186
+ /// Reset the centre with {setNavGuardSnapshot} before depositing into that resource
187
+ /// again.
188
+ /// @param resource Resource whose band could not record the deposit.
189
+ /// @param centre Centre the deposit would have set.
190
+ error NavGuardCentreOverflow(address resource, uint256 centre);
191
+
192
+ // ----------------------- Governance ---------------------------------------
193
+
194
+ /**
195
+ * @notice Configure the band for one resource and arm the sides asked for: valuation will
196
+ * report its value clamped into a corridor that drifts `driftBps` per year, `upGapBps`
197
+ * wide above the centre and `downGapBps` below.
198
+ * @dev ACM role `"setNavGuardRate(address,uint16,uint16,uint16,uint32,bool,bool)"` (VIP).
199
+ * Re-callable to retune. Reverts {InvalidNavGuardRate} on a zero `interval` or a gap of
200
+ * 100% or more, {NavGuardUnreadable} when the position cannot be valued. Emits
201
+ * {NavGuardConfigured}.
202
+ *
203
+ * Two things a retune does that are easy to miss. It sets both flags from its arguments
204
+ * instead of keeping the current pair, so a cap-only deployment turns the floor back on by
205
+ * passing the wrong ones. And it re-anchors on the value observed right now, held inside
206
+ * the band already in force, so a retune cannot adopt a value the band was rejecting and a
207
+ * closed band stays closed through it. A position that reads empty is not held to the old
208
+ * band: there is nothing to vouch for, so it anchors a closed band. Accepting a real
209
+ * markdown, or reopening a closed band, is {setNavGuardSnapshot}'s job.
210
+ *
211
+ * A zero observed value is accepted, so a band can be armed before its resource is funded.
212
+ * It is then closed until the first deposit opens it, and that deposit is reported at
213
+ * exactly what went in until the first re-anchor gives the band an anchor to size its gaps
214
+ * from.
215
+ *
216
+ * Sizing. `driftBps` is the position's expected annual return, set above its real trend or
217
+ * the cap binds on ordinary growth. The gaps are how far a wrong value moves valuation
218
+ * before it is clamped: `upGapBps` covers the jump when the counterparty settles at its
219
+ * own mark, `downGapBps` the worst single honest decline. Zero is a side's tightest
220
+ * setting, not an off switch.
221
+ *
222
+ * `downGapBps` cannot be picked without `interval`. Each re-anchor lifts the anchor by one
223
+ * interval of drift before the gap takes it back down, so the anchor falls only while
224
+ * `downGapBps` exceeds `driftBps * interval / 365 days` — at 8% drift, a 2% gap needs an
225
+ * interval under about three months. Under that the anchor climbs against a position that
226
+ * is genuinely losing, floor armed or not, since the re-anchor clamps against both bounds
227
+ * whatever the flags say.
228
+ *
229
+ * The cap bounds the rate a wrong value can inflate valuation, not the total: one that
230
+ * stays wrong advances the cap by its full width every `interval`.
231
+ * @param resource Registered resource the band applies to.
232
+ * @param driftBps Annual drift of the anchor, in basis points. May be zero.
233
+ * @param upGapBps Half-width above the centre, in basis points. May be zero, must be under
234
+ * 100%.
235
+ * @param downGapBps Half-width below the centre, in basis points. Must be under 100%.
236
+ * @param interval Minimum seconds between periodic re-anchors. Must be non-zero.
237
+ * @param capEnabled True to clamp valuation down to the cap.
238
+ * @param floorEnabled True to clamp valuation up to the floor.
239
+ */
240
+ function setNavGuardRate(
241
+ address resource,
242
+ uint16 driftBps,
243
+ uint16 upGapBps,
244
+ uint16 downGapBps,
245
+ uint32 interval,
246
+ bool capEnabled,
247
+ bool floorEnabled
248
+ ) external;
249
+
250
+ /**
251
+ * @notice Force the band's anchor and centre onto one value, and its clock onto one time,
252
+ * without touching either gap or `interval`.
253
+ * @dev ACM role `"setNavGuardSnapshot(address,uint128,uint64)"` (VIP). Reads no value — use it
254
+ * to accept a markdown the floor is holding up, to reopen a band a withdrawal closed, or
255
+ * to hold the previous centre through an investigation. Reverts {InvalidNavGuardSnapshot}
256
+ * if the band was never configured, `snapshot` is zero, or `timestamp` is zero or in the
257
+ * future. Emits {NavGuardReanchored}.
258
+ *
259
+ * `timestamp` is both the drift baseline and the re-anchor clock. A backdated one has
260
+ * already accrued drift, so the centre this puts in force is above `snapshot`, and
261
+ * backdating by a full `interval` also leaves a re-anchor due immediately. Nothing here
262
+ * compares the centre to the observed value, so parking the band away from it is allowed
263
+ * and clamps valuation until the two meet.
264
+ *
265
+ * A zero `snapshot` is refused rather than treated as an off switch: it would close the
266
+ * band by hand, and {setNavGuardEnabled} already expresses switching the band off, under
267
+ * its own role. To write a resource off entirely, that is `forceRemoveResource`.
268
+ * @param resource Registered resource whose centre to force.
269
+ * @param snapshot New anchor and centre, in asset units.
270
+ * @param timestamp Timestamp to drift from, and what the `interval` clock is measured from.
271
+ */
272
+ function setNavGuardSnapshot(address resource, uint128 snapshot, uint64 timestamp) external;
273
+
274
+ /**
275
+ * @notice Arm either side of a resource's band, both, or neither, keeping its configuration
276
+ * whichever way.
277
+ * @dev ACM role `"setNavGuardEnabled(address,bool,bool)"` (VIP). Reads no value, so it still
278
+ * works when the value source is broken — which is the point, since a band clamping a
279
+ * healthy position is exactly when it has to come off in one transaction. Reverts
280
+ * {NavGuardNotConfigured} when arming a side of a band {setNavGuardRate} has never run on.
281
+ * Idempotent. Emits {NavGuardEnabledSet}.
282
+ *
283
+ * The two sides do different jobs, which is why they arm separately. The cap protects the
284
+ * Hub: an over-reported position is valued down, so nobody withdraws against value that is
285
+ * not there. The floor does the opposite — it reports more than the position is worth, in
286
+ * exchange for marking a loss down gradually instead of at once. A deployment that wants
287
+ * the protection without that trade runs cap-only, and both false switches the band off
288
+ * without discarding it.
289
+ *
290
+ * The flags do not gate the centre. Our own moves and the re-anchor apply either way, and
291
+ * the re-anchor clamps against both bounds, so a side switched off cannot let a wrong
292
+ * value re-anchor the band, and arming it again needs no second transaction. A closed
293
+ * band is the exception: it stays closed until a deposit or {setNavGuardSnapshot} reopens
294
+ * it.
295
+ * @param resource Registered resource whose band to arm.
296
+ * @param capEnabled True to clamp valuation down to the cap.
297
+ * @param floorEnabled True to clamp valuation up to the floor.
298
+ */
299
+ function setNavGuardEnabled(address resource, bool capEnabled, bool floorEnabled) external;
300
+
301
+ // ----------------------- Views --------------------------------------------
302
+
303
+ /**
304
+ * @notice The band configured for `resource`, whether or not either side is armed.
305
+ * @param resource Resource to read.
306
+ * @return band Stored configuration; all-zero when never configured.
307
+ */
308
+ function navGuard(address resource) external view returns (NavBand memory band);
309
+
310
+ /**
311
+ * @notice Where `resource`'s value stands against its band right now.
312
+ * @dev Never reverts, so monitoring can read it whether or not valuation is working. An
313
+ * unreadable source reports `observedValue` as zero, and everything reads zero and false
314
+ * when the band was never configured.
315
+ *
316
+ * Both bounds are reported armed or not, so monitoring can see where a side would bind
317
+ * before arming it. `isClamped` follows only what valuation actually does, so it is false
318
+ * when the breached side is unarmed, and false for a zero `observedValue` — which is
319
+ * either an unreadable source or an empty position, and valuation passes both through. A
320
+ * closed band under a nonzero `observedValue` is a real clamp to nothing, and says the
321
+ * resource holds something valuation is not counting.
322
+ * @param resource Resource to read.
323
+ * @return observedValue Value the counterparty reports, in asset units; `0` if unreadable.
324
+ * @return minAllowedValue Lowest value the band allows right now.
325
+ * @return maxAllowedValue Highest value the band allows right now.
326
+ * @return isClamped Whether valuation is currently reporting a bound instead of `observedValue`.
327
+ * @return clampedValue What valuation reports instead; `0` when `isClamped` is false.
328
+ */
329
+ function navGuardStatus(address resource)
330
+ external
331
+ view
332
+ returns (
333
+ uint256 observedValue,
334
+ uint256 minAllowedValue,
335
+ uint256 maxAllowedValue,
336
+ bool isClamped,
337
+ uint256 clampedValue
338
+ );
339
+ }
@@ -0,0 +1,248 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ import { INavGuard } from "./INavGuard.sol";
5
+ import { IYieldGroupBase } from "./IYieldGroupBase.sol";
6
+
7
+ /**
8
+ * @title IYieldGroupCentrifuge
9
+ * @author Venus
10
+ * @notice External interface for `YieldGroupCentrifuge` — a per-asset YieldGroup holding
11
+ * Centrifuge async tokenized-fund vaults (ERC-7540) behind the synchronous Hub boundary.
12
+ * @dev Extends `IYieldGroupBase` with the async lifecycle the family needs: operator-driven
13
+ * redeem requests and cancels, keeper-driven claims. Deposits need no new surface — the
14
+ * standard deposit path already opens an escrow request synchronously. Governance also
15
+ * gets: the per-resource value band from `INavGuard`, which clamps a Centrifuge position's
16
+ * reported value on both sides, with a manual override for when the observed value can't be
17
+ * trusted; and a write-off removal for a position Centrifuge has made permanently
18
+ * unclaimable.
19
+ */
20
+ interface IYieldGroupCentrifuge is IYieldGroupBase, INavGuard {
21
+ // -------------------------------- Events ----------------------------------
22
+
23
+ /**
24
+ * @notice An async redeem request was opened: `shares` moved into Centrifuge escrow.
25
+ * @param resource Centrifuge vault the request was opened on.
26
+ * @param shares Share units moved into escrow.
27
+ */
28
+ event RedeemRequested(address indexed resource, uint256 shares);
29
+
30
+ /**
31
+ * @notice A cancel was requested for the pending deposit; new deposit requests are blocked
32
+ * until Centrifuge fulfils the cancel.
33
+ * @param resource Centrifuge vault whose pending deposit is being cancelled.
34
+ */
35
+ event DepositCancelRequested(address indexed resource);
36
+
37
+ /**
38
+ * @notice A cancel was requested for the pending redeem.
39
+ * @param resource Centrifuge vault whose pending redeem is being cancelled.
40
+ */
41
+ event RedeemCancelRequested(address indexed resource);
42
+
43
+ /**
44
+ * @notice Issued shares were claimed into the group.
45
+ * @param resource Centrifuge vault the shares were claimed from.
46
+ * @param shares Share units delivered to the group.
47
+ */
48
+ event DepositClaimed(address indexed resource, uint256 shares);
49
+
50
+ /**
51
+ * @notice Settled assets were claimed into the group as idle.
52
+ * @dev Only {claimRedeem} emits this. A Hub withdrawal that lands on a settled redeem draws
53
+ * down the same Centrifuge counter but is logged as {IYieldGroupBase.WithdrawRouted}, so
54
+ * this event alone does not add up to every claim.
55
+ * @param resource Centrifuge vault the assets were claimed from.
56
+ * @param assets Asset units delivered to the group.
57
+ */
58
+ event RedeemClaimed(address indexed resource, uint256 assets);
59
+
60
+ /**
61
+ * @notice A fulfilled deposit cancel was collected as idle assets.
62
+ * @param resource Centrifuge vault the cancelled deposit returned from.
63
+ * @param assets Asset units returned to the group.
64
+ */
65
+ event DepositCancelClaimed(address indexed resource, uint256 assets);
66
+
67
+ /**
68
+ * @notice A fulfilled redeem cancel was collected as held shares.
69
+ * @param resource Centrifuge vault the cancelled redeem returned from.
70
+ * @param shares Share units returned to the group.
71
+ */
72
+ event RedeemCancelClaimed(address indexed resource, uint256 shares);
73
+
74
+ /**
75
+ * @notice Governance wrote off a permanently unclaimable position and dropped the resource.
76
+ * @param resource Force-removed Centrifuge vault.
77
+ * @param writtenOffValue Underlying value the position was still marked at when removed, raw
78
+ * rather than clamped to the NAV band — the point of a write-off is to record what was
79
+ * actually there. `0` when valuation itself reverted, which is a dead price feed, and
80
+ * plausible in the situation that led here: the value was unknowable at removal time,
81
+ * not necessarily zero.
82
+ * @param orphanedReceipt Receipt sentinel (held shares plus in-flight counters) abandoned.
83
+ */
84
+ event ResourceForceRemoved(address indexed resource, uint256 writtenOffValue, uint256 orphanedReceipt);
85
+
86
+ /**
87
+ * @notice Governance published a new spot APY for one Centrifuge vault.
88
+ * @param resource Centrifuge vault the rate was published for.
89
+ * @param oldApyBps Rate the resource was advertising before this call.
90
+ * @param newApyBps Rate it advertises from now on.
91
+ */
92
+ event SpotAPYBpsSet(address indexed resource, uint64 oldApyBps, uint64 newApyBps);
93
+
94
+ // -------------------------------- Errors ----------------------------------
95
+
96
+ /// @notice `requestRedeem()` was called with zero shares.
97
+ error ZeroShares();
98
+
99
+ /// @notice `forceRemoveResource()` was called while the Hub was unpaused. The write-off drops the
100
+ /// Hub's share price in one block; the pause keeps that drop untradeable.
101
+ error HubNotPaused();
102
+
103
+ // ----------------------- Initialisation -----------------------------------
104
+
105
+ /**
106
+ * @notice Initialise the YieldGroup. Called once via the proxy.
107
+ * @param hub_ Hub authorised to call `deposit` / `withdraw`.
108
+ * @param asset_ Underlying ERC-20 (must match `Hub.asset()`).
109
+ * @param acm_ AccessControlManagerV8 gating governance calls.
110
+ */
111
+ function initialize(address hub_, address asset_, address acm_) external;
112
+
113
+ // ----------------------- Async lifecycle (ACM-gated) ----------------------
114
+
115
+ /**
116
+ * @notice Open an async redeem request: move `shares` of the vault's share token from this
117
+ * group into Centrifuge escrow. The settled assets become collectable via
118
+ * {claimRedeem} once the fund manager fills the request cross-chain.
119
+ * @dev ACM role `"requestRedeem(address,uint256)"` (Operator or VIP). Allowed on a paused
120
+ * resource: pause blocks new deposits, not wind-down. Reverts {ZeroShares} on a zero
121
+ * amount, {ResourceNotRegistered} on an unknown resource. Emits {RedeemRequested}.
122
+ * @param resource Registered Centrifuge vault to open the request on.
123
+ * @param shares Share units to move into escrow for redemption.
124
+ */
125
+ function requestRedeem(address resource, uint256 shares) external;
126
+
127
+ /**
128
+ * @notice Ask Centrifuge to return the group's pending deposit. The returned assets become
129
+ * collectable via {claimCancelDeposit} once the manager processes the cancel;
130
+ * until then new deposit requests to this vault are blocked.
131
+ * @dev ACM role `"cancelDepositRequest(address)"` (Operator or VIP). Reverts
132
+ * {ResourceNotRegistered} on an unknown resource; Centrifuge itself rejects a cancel
133
+ * with nothing pending or with one already in flight. Emits {DepositCancelRequested}.
134
+ * @param resource Registered Centrifuge vault whose pending deposit to cancel.
135
+ */
136
+ function cancelDepositRequest(address resource) external;
137
+
138
+ /**
139
+ * @notice Ask Centrifuge to return the group's pending redeem shares. The returned shares
140
+ * become collectable via {claimCancelRedeem} once the manager processes the cancel.
141
+ * @dev ACM role `"cancelRedeemRequest(address)"` (Operator or VIP). Reverts
142
+ * {ResourceNotRegistered} on an unknown resource; Centrifuge itself rejects a cancel
143
+ * with nothing pending or with one already in flight. Emits {RedeemCancelRequested}.
144
+ * @param resource Registered Centrifuge vault whose pending redeem to cancel.
145
+ */
146
+ function cancelRedeemRequest(address resource) external;
147
+
148
+ /**
149
+ * @notice Claim every share issued by the group's filled deposit requests into the group.
150
+ * @dev ACM role `"claimDeposit(address)"` (Keeper or VIP). Idempotent: nothing claimable is a
151
+ * silent no-op with no event, so the keeper can poll safely. Reverts
152
+ * {ResourceNotRegistered} on an unknown resource. Emits {DepositClaimed} when
153
+ * `shares > 0`.
154
+ * @param resource Registered Centrifuge vault to claim issued shares from.
155
+ * @return shares Share units claimed (`0` when nothing was claimable).
156
+ */
157
+ function claimDeposit(address resource) external returns (uint256 shares);
158
+
159
+ /**
160
+ * @notice Claim every asset reserved by the group's filled redeem requests into the group
161
+ * as idle balance.
162
+ * @dev ACM role `"claimRedeem(address)"` (Keeper or VIP). Idempotent: nothing claimable is a
163
+ * silent no-op with no event, so the keeper can poll safely. Reverts
164
+ * {ResourceNotRegistered} on an unknown resource. Emits {RedeemClaimed} when
165
+ * `assets > 0`.
166
+ * @param resource Registered Centrifuge vault to claim settled assets from.
167
+ * @return assets Asset units that reached the group (`0` when nothing was claimable).
168
+ */
169
+ function claimRedeem(address resource) external returns (uint256 assets);
170
+
171
+ /**
172
+ * @notice Collect the assets a processed deposit cancel returned, into the group as idle
173
+ * balance.
174
+ * @dev ACM role `"claimCancelDeposit(address)"` (Keeper or VIP). Centrifuge lifts the block on
175
+ * new deposit requests when it fulfils the cancel, not when this is called — the claim
176
+ * only collects the returned value. Idempotent: nothing claimable is a silent no-op with
177
+ * no event. Reverts {ResourceNotRegistered} on an unknown resource. Emits
178
+ * {DepositCancelClaimed} when `assets > 0`.
179
+ * @param resource Registered Centrifuge vault to collect the cancelled deposit from.
180
+ * @return assets Asset units that reached the group (`0` when nothing was claimable).
181
+ */
182
+ function claimCancelDeposit(address resource) external returns (uint256 assets);
183
+
184
+ /**
185
+ * @notice Collect the shares a processed redeem cancel returned, back into the group's held
186
+ * share balance.
187
+ * @dev ACM role `"claimCancelRedeem(address)"` (Keeper or VIP). Centrifuge lifts the block on
188
+ * new redeem requests when it fulfils the cancel, not when this is called — the claim
189
+ * only collects the returned value. Idempotent: nothing claimable is a silent no-op with
190
+ * no event. Reverts {ResourceNotRegistered} on an unknown resource. Emits
191
+ * {RedeemCancelClaimed} when `shares > 0`.
192
+ * @param resource Registered Centrifuge vault to collect the cancelled redeem from.
193
+ * @return shares Share units collected (`0` when nothing was claimable).
194
+ */
195
+ function claimCancelRedeem(address resource) external returns (uint256 shares);
196
+
197
+ // ----------------------- Write-off removal (governance) -------------------
198
+
199
+ /**
200
+ * @notice Write off a permanently unclaimable position and drop the resource from the registry.
201
+ * @dev ACM role `"forceRemoveResource(address)"` (VIP). For the case normal `removeResource`
202
+ * cannot reach: once Centrifuge freezes the group's address or unlinks the vault, every
203
+ * claim reverts and the position can never be emptied, so leaving it registered would keep
204
+ * propping the Hub's share price up with money that is not coming back.
205
+ *
206
+ * Unlike `YieldGroupFRV.forceRemoveResource` there is deliberately no zero-value gate — a
207
+ * frozen Centrifuge position still marks above zero, so requiring NAV `== 0` would make
208
+ * this unusable in exactly the situation it exists for. CAUTION: with no value check of any
209
+ * kind, this removes a healthy, fully recoverable resource just as readily as a stuck one,
210
+ * and governance is accepting a real write-off either way — the Hub's share price drops by
211
+ * the removed value in this transaction. Establishing that the position is genuinely
212
+ * unclaimable is the caller's responsibility; pausing the resource first is the recommended
213
+ * practice. The Hub itself must be paused or the call reverts {HubNotPaused}, so the sequence
214
+ * is `pauseHub` -> `forceRemoveResource` -> `unpauseHub`. Clears both inner queues and the
215
+ * resource's NAV band. Emits {ResourceForceRemoved} carrying the value written off — the
216
+ * raw value, not the band-clamped one, since a write-off should record what was actually
217
+ * there.
218
+ * @param resource Registered Centrifuge vault to write off and remove.
219
+ */
220
+ function forceRemoveResource(address resource) external;
221
+
222
+ // ----------------------- Published spot APY (governance) ------------------
223
+
224
+ /**
225
+ * @notice Publish the annual rate a Centrifuge fund is yielding, in basis points
226
+ * (`10_000` = 100%), so `spotAPYBps()` has something to report for it. Centrifuge
227
+ * computes its funds' returns off-chain and puts no rate on-chain, so this call is the
228
+ * only source that number has. `0` publishes none, which is what a resource reports
229
+ * until someone sets it.
230
+ * @dev ACM role `"setSpotAPYBps(address,uint64)"` (Operator or VIP). The published rate reaches
231
+ * `spotAPYBps()` and stops there — not share pricing, not routing, not caps, not fees — so
232
+ * a wrong figure misstates the advertised yield and cannot move anyone's balance. Nothing
233
+ * on-chain can check it against the fund, and no value is rejected, so what governance
234
+ * types is what the group advertises. Reverts {ResourceNotRegistered} on a vault this
235
+ * group does not manage. Removing the resource clears the rate. Emits {SpotAPYBpsSet}.
236
+ * @param resource Registered Centrifuge vault the rate belongs to.
237
+ * @param apyBps Annual rate in basis points, or `0` to publish none.
238
+ */
239
+ function setSpotAPYBps(address resource, uint64 apyBps) external;
240
+
241
+ /**
242
+ * @notice The rate governance has published for one vault, before it is weighted into the
243
+ * group's `spotAPYBps()`.
244
+ * @param resource Centrifuge vault to look up.
245
+ * @return apyBps Published annual rate in basis points; `0` when none is published.
246
+ */
247
+ function resourceSpotAPYBps(address resource) external view returns (uint64 apyBps);
248
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.25;
3
+
4
+ /**
5
+ * @title ICentrifugeAsyncRequestManager
6
+ * @author Venus
7
+ * @notice The one read `AdapterCentrifuge` needs from Centrifuge's `AsyncRequestManager`: the
8
+ * per-position `AsyncInvestmentState` behind every bucket of the adapter's valuation and
9
+ * every claim amount.
10
+ * @dev The struct is declared inline rather than imported from the Centrifuge repo, to avoid an
11
+ * npm dependency. Field order and types MUST mirror Centrifuge's `AsyncInvestmentState`
12
+ * exactly — they are ABI-load-bearing. Two details make that mirror hold: Centrifuge's `D18`
13
+ * price fields are `uint128` underneath, and the real member is a public mapping, whose
14
+ * getter encodes identically to this struct-returning function.
15
+ */
16
+ interface ICentrifugeAsyncRequestManager {
17
+ // ──────────────────────────── Types ─────────────────────────────
18
+
19
+ /// @notice Per-(vault, controller) position state, mirroring `AsyncInvestmentState` in the
20
+ /// Centrifuge repo field-for-field. The buckets are disjoint: a cancelled slice is
21
+ /// already deducted from the pending counters.
22
+ struct AsyncInvestmentState {
23
+ // ── Fulfilled: issued/revoked into escrow, only the claim is left ──
24
+ uint128 maxMint; // shares already issued, claimable via `mint` (share decimals)
25
+ uint128 maxWithdraw; // assets already revoked, claimable via `withdraw` (asset decimals)
26
+ uint128 depositPrice; // D18: weighted avg asset-per-share of the fills behind `maxMint`
27
+ uint128 redeemPrice; // D18: same, for the fills behind `maxWithdraw`
28
+ // ── Pending: paid in, not yet fulfilled ──
29
+ uint128 pendingDepositRequest; // assets sent in, nothing issued yet (asset decimals)
30
+ uint128 pendingRedeemRequest; // shares sent in, nothing revoked yet (share decimals)
31
+ // ── Cancelled: returned instead of filled ──
32
+ uint128 claimableCancelDepositRequest; // assets coming back (asset decimals)
33
+ uint128 claimableCancelRedeemRequest; // shares coming back (share decimals)
34
+ bool pendingCancelDepositRequest; // cancel in flight — blocks new deposit requests
35
+ bool pendingCancelRedeemRequest; // cancel in flight — blocks new redeem requests
36
+ }
37
+
38
+ // ──────────────────────────── Views ─────────────────────────────
39
+
40
+ /// @notice Full position state for `controller` on `vault` — the source the adapter values the
41
+ /// position from, sizes every claim against, and reads the cancel-in-flight gates off.
42
+ function investments(address vault, address controller) external view returns (AsyncInvestmentState memory state);
43
+ }