@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,358 @@
|
|
|
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 { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
6
|
+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
7
|
+
|
|
8
|
+
import { IAdapterCentrifuge } from "../interfaces/IAdapterCentrifuge.sol";
|
|
9
|
+
import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
|
|
10
|
+
import { IYieldGroupBase } from "../interfaces/IYieldGroupBase.sol";
|
|
11
|
+
import { IYieldGroupCentrifuge } from "../interfaces/IYieldGroupCentrifuge.sol";
|
|
12
|
+
import { ICentrifugeAsyncRequestManager } from "../interfaces/external/ICentrifugeAsyncRequestManager.sol";
|
|
13
|
+
import { ICentrifugeAsyncVault } from "../interfaces/external/ICentrifugeAsyncVault.sol";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @title AdapterCentrifuge
|
|
17
|
+
* @author Venus
|
|
18
|
+
* @notice Stateless adapter for Centrifuge ERC-7540 vaults such as JTRSY and JAAA.
|
|
19
|
+
* A single deployment can serve any YieldGroup that registers a compatible vault.
|
|
20
|
+
* @dev Mutating functions execute by delegatecall from the YieldGroup. Under delegatecall, the
|
|
21
|
+
* YieldGroup is used as the controller, owner, and claim receiver.
|
|
22
|
+
*
|
|
23
|
+
* A position includes the held share balance and six asynchronous request counters. Asset
|
|
24
|
+
* amounts are valued at face value, while share amounts are converted at the vault's NAV:
|
|
25
|
+
* - face value: `pendingDepositRequest`, `claimableCancelDepositRequest`, `maxWithdraw`;
|
|
26
|
+
* - NAV value: held shares, `maxMint`, `pendingRedeemRequest`,
|
|
27
|
+
* `claimableCancelRedeemRequest`.
|
|
28
|
+
* Idle assets are excluded because the YieldGroup adds them separately.
|
|
29
|
+
*
|
|
30
|
+
* Claims use the same denomination as the claimable counter: `mint(maxMint)` for shares and
|
|
31
|
+
* `withdraw(maxWithdraw)` for assets. Using `deposit` or `redeem` would convert from the
|
|
32
|
+
* opposite denomination and could leave one smallest unit unclaimed because of rounding.
|
|
33
|
+
*
|
|
34
|
+
* {totalAssets} values the position from Centrifuge's own counters and price. It rejects a
|
|
35
|
+
* zero share price whenever the position contains shares. The group clamps what comes back to
|
|
36
|
+
* that resource's NAV band, and it is the group's `totalAssets()` that the Hub sums. That band
|
|
37
|
+
* is configured there, since this adapter is stateless.
|
|
38
|
+
*
|
|
39
|
+
* Asset moves read the underlying from the calling YieldGroup, never from `resource.asset()`;
|
|
40
|
+
* see {_groupAsset}.
|
|
41
|
+
*/
|
|
42
|
+
contract AdapterCentrifuge is IAdapterCentrifuge {
|
|
43
|
+
using SafeERC20 for IERC20;
|
|
44
|
+
|
|
45
|
+
// ============================== Constants =================================
|
|
46
|
+
|
|
47
|
+
/// @dev Centrifuge assigns request id 0 to every request. Requests for the same vault and
|
|
48
|
+
/// controller accumulate in one `AsyncInvestmentState` instead of receiving individual
|
|
49
|
+
/// ids, so cancellation and cancel-claim calls always target id 0.
|
|
50
|
+
uint256 private constant REQUEST_ID = 0;
|
|
51
|
+
|
|
52
|
+
// ============================== State (immutable only) ====================
|
|
53
|
+
|
|
54
|
+
/// @dev Adapter deployment address used to distinguish direct calls from delegatecalls.
|
|
55
|
+
address private immutable _ADAPTER_SELF;
|
|
56
|
+
|
|
57
|
+
// ============================== Errors ====================================
|
|
58
|
+
|
|
59
|
+
/// @notice Thrown when a delegatecall-only function is called directly on the adapter.
|
|
60
|
+
error NotDelegateCall();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @notice Thrown when a resource does not expose a share token.
|
|
64
|
+
* @param resource Resource that failed validation.
|
|
65
|
+
*/
|
|
66
|
+
error NoShareToken(address resource);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @notice Thrown when a resource does not expose a request manager.
|
|
70
|
+
* @param resource Resource that failed validation.
|
|
71
|
+
*/
|
|
72
|
+
error NoManager(address resource);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @notice Thrown when a position contains shares but the vault reports a zero share price.
|
|
76
|
+
* @dev `requestRedeem` does not clear this: it still works, but a pending redeem is counted as
|
|
77
|
+
* shares. This clears when Centrifuge settles that redeem and the value lands in
|
|
78
|
+
* `maxWithdraw`, which is in asset units. If Centrifuge never settles,
|
|
79
|
+
* `forceRemoveResource` writes the position off.
|
|
80
|
+
* @param resource Resource whose share price is zero.
|
|
81
|
+
*/
|
|
82
|
+
error ZeroSharePrice(address resource);
|
|
83
|
+
|
|
84
|
+
// ============================== Modifiers =================================
|
|
85
|
+
|
|
86
|
+
/// @dev Restricts mutating adapter functions to delegatecall execution.
|
|
87
|
+
modifier onlyDelegateCall() {
|
|
88
|
+
if (address(this) == _ADAPTER_SELF) revert NotDelegateCall();
|
|
89
|
+
_;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================== Constructor ===============================
|
|
93
|
+
|
|
94
|
+
/// @dev Stores the adapter address used by {onlyDelegateCall}.
|
|
95
|
+
constructor() {
|
|
96
|
+
_ADAPTER_SELF = address(this);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================== IResourceAdapter — mutating ===============
|
|
100
|
+
|
|
101
|
+
/// @inheritdoc IResourceAdapter
|
|
102
|
+
/// @dev Sends assets to Centrifuge as an asynchronous deposit request. Shares can be collected
|
|
103
|
+
/// through {claimDeposit} after settlement. The temporary allowance supports vaults that
|
|
104
|
+
/// pull assets with `transferFrom`.
|
|
105
|
+
function deposit(address resource, uint256 amount) external override onlyDelegateCall returns (uint256) {
|
|
106
|
+
IERC20 assetToken = _groupAsset();
|
|
107
|
+
assetToken.forceApprove(resource, amount);
|
|
108
|
+
ICentrifugeAsyncVault(resource).requestDeposit(amount, address(this), address(this));
|
|
109
|
+
assetToken.forceApprove(resource, 0);
|
|
110
|
+
return amount;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// @inheritdoc IResourceAdapter
|
|
114
|
+
/// @dev Withdraws assets already reserved in `maxWithdraw`. Invested or pending shares are not
|
|
115
|
+
/// liquid until a redeem request settles. The YieldGroup sizes normal calls against
|
|
116
|
+
/// `maxWithdraw`; the vault call remains the final enforcement point.
|
|
117
|
+
///
|
|
118
|
+
/// The withdrawal uses two steps because the vault permission-checks the claim receiver.
|
|
119
|
+
/// Claiming directly to an arbitrary `to` may revert if that address is not on the share-class
|
|
120
|
+
/// memberlist. The adapter therefore claims to the group as receiver and controller, then
|
|
121
|
+
/// forwards the underlying with a regular ERC-20 transfer.
|
|
122
|
+
///
|
|
123
|
+
/// This consumes the same `maxWithdraw` counter as {claimRedeem}, but the YieldGroup logs
|
|
124
|
+
/// it as `WithdrawRouted`, not `RedeemClaimed`.
|
|
125
|
+
function withdraw(address resource, uint256 amount, address to) external override onlyDelegateCall {
|
|
126
|
+
ICentrifugeAsyncVault vault = ICentrifugeAsyncVault(resource);
|
|
127
|
+
IERC20 assetToken = _groupAsset();
|
|
128
|
+
|
|
129
|
+
// Forward only what the vault delivered. Forwarding `amount` could use sufficient idle
|
|
130
|
+
// balance to hide a short delivery from the caller's recipient-balance check. Preserving the
|
|
131
|
+
// shortfall makes the caller revert instead.
|
|
132
|
+
uint256 preBal = assetToken.balanceOf(address(this));
|
|
133
|
+
vault.withdraw(amount, address(this), address(this));
|
|
134
|
+
uint256 received = assetToken.balanceOf(address(this)) - preBal;
|
|
135
|
+
|
|
136
|
+
if (to != address(this)) assetToken.safeTransfer(to, received);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// @inheritdoc IResourceAdapter
|
|
140
|
+
/// @dev No-op because Centrifuge state is valued live. Claims occur through the YieldGroup's
|
|
141
|
+
/// explicit claim functions or when a withdrawal reaches a settled resource.
|
|
142
|
+
// solhint-disable-next-line no-empty-blocks
|
|
143
|
+
function accrue(address resource) external override { }
|
|
144
|
+
|
|
145
|
+
// ============================== Centrifuge-only — mutating ================
|
|
146
|
+
|
|
147
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
148
|
+
/// @dev Sends shares to Centrifuge as an asynchronous redeem request. Assets can be collected
|
|
149
|
+
/// through {claimRedeem} after settlement. No share approval is needed: Centrifuge's
|
|
150
|
+
/// BalanceSheet moves the shares with `authTransferFrom`, a ward call that never reads
|
|
151
|
+
/// `allowance`. Deposits are different — there the vault runs a real `transferFrom` on the
|
|
152
|
+
/// asset, so that approval stays.
|
|
153
|
+
function requestRedeem(address resource, uint256 shares) external override onlyDelegateCall {
|
|
154
|
+
ICentrifugeAsyncVault(resource).requestRedeem(shares, address(this), address(this));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
158
|
+
function cancelDepositRequest(address resource) external override onlyDelegateCall {
|
|
159
|
+
ICentrifugeAsyncVault(resource).cancelDepositRequest(REQUEST_ID, address(this));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
163
|
+
function cancelRedeemRequest(address resource) external override onlyDelegateCall {
|
|
164
|
+
ICentrifugeAsyncVault(resource).cancelRedeemRequest(REQUEST_ID, address(this));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
168
|
+
/// @dev Requests the full `maxMint` amount to clear the claimable share balance.
|
|
169
|
+
function claimDeposit(address resource) external override onlyDelegateCall returns (uint256 shares) {
|
|
170
|
+
shares = _investments(resource, address(this)).maxMint;
|
|
171
|
+
if (shares > 0) {
|
|
172
|
+
ICentrifugeAsyncVault(resource).mint(shares, address(this), address(this));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
177
|
+
/// @dev Requests the full `maxWithdraw` amount to clear the claimable asset balance.
|
|
178
|
+
function claimRedeem(address resource) external override onlyDelegateCall returns (uint256 assets) {
|
|
179
|
+
assets = _investments(resource, address(this)).maxWithdraw;
|
|
180
|
+
if (assets > 0) {
|
|
181
|
+
ICentrifugeAsyncVault(resource).withdraw(assets, address(this), address(this));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
186
|
+
function claimCancelDeposit(address resource) external override onlyDelegateCall returns (uint256 assets) {
|
|
187
|
+
assets = _investments(resource, address(this)).claimableCancelDepositRequest;
|
|
188
|
+
if (assets > 0) {
|
|
189
|
+
ICentrifugeAsyncVault(resource).claimCancelDepositRequest(REQUEST_ID, address(this), address(this));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
194
|
+
function claimCancelRedeem(address resource) external override onlyDelegateCall returns (uint256 shares) {
|
|
195
|
+
shares = _investments(resource, address(this)).claimableCancelRedeemRequest;
|
|
196
|
+
if (shares > 0) {
|
|
197
|
+
ICentrifugeAsyncVault(resource).claimCancelRedeemRequest(REQUEST_ID, address(this), address(this));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ============================== Views ======================================
|
|
202
|
+
|
|
203
|
+
/// @inheritdoc IResourceAdapter
|
|
204
|
+
function asset(address resource) external view override returns (address) {
|
|
205
|
+
return ICentrifugeAsyncVault(resource).asset();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// @inheritdoc IResourceAdapter
|
|
209
|
+
/// @dev What Centrifuge's counters and price say the position is worth. `YieldGroupCentrifuge`
|
|
210
|
+
/// clamps this to the resource's NAV band on the way to the Hub, so read the group's
|
|
211
|
+
/// `totalAssets()` for the figure the Hub sums, and this one for what the band is clamping.
|
|
212
|
+
///
|
|
213
|
+
/// Only the share-denominated buckets need a price, so a position holding none is valued
|
|
214
|
+
/// without the feed at all.
|
|
215
|
+
function totalAssets(address resource, address holder) external view override returns (uint256 value) {
|
|
216
|
+
(uint256 face, uint256 shares, address shareToken) = _assetAndShareTotals(resource, holder);
|
|
217
|
+
value = face;
|
|
218
|
+
if (shares > 0) {
|
|
219
|
+
uint256 price = _pricePerShareOrZero(resource);
|
|
220
|
+
// Reject an unset or unreadable price before it silently values a live position at zero.
|
|
221
|
+
if (price == 0) revert ZeroSharePrice(resource);
|
|
222
|
+
value += (shares * price) / (10 ** IERC20Metadata(shareToken).decimals());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// @inheritdoc IResourceAdapter
|
|
227
|
+
/// @dev The uint128 limit Centrifuge's counters impose. Nothing calls this for Centrifuge:
|
|
228
|
+
/// `YieldGroupCentrifuge` sizes deposits through {depositRoom}, which also sees the holder.
|
|
229
|
+
/// Kept to satisfy `IResourceAdapter`.
|
|
230
|
+
function maxDeposit(address resource) external pure override returns (uint256) {
|
|
231
|
+
resource;
|
|
232
|
+
return type(uint128).max;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
236
|
+
function depositRoom(address resource, address holder) external view override returns (uint256) {
|
|
237
|
+
ICentrifugeAsyncRequestManager.AsyncInvestmentState memory inv = _investments(resource, holder);
|
|
238
|
+
if (inv.pendingCancelDepositRequest) return 0;
|
|
239
|
+
return type(uint128).max - inv.pendingDepositRequest;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// @inheritdoc IAdapterCentrifuge
|
|
243
|
+
function pricePerShare(address resource) external view override returns (uint256 price) {
|
|
244
|
+
return ICentrifugeAsyncVault(resource).pricePerShare();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// @inheritdoc IResourceAdapter
|
|
248
|
+
/// @dev Returns the vault's `maxWithdraw(holder)`, so the vault's permission checks are reflected
|
|
249
|
+
/// in reported liquidity. Valuation deliberately reads the raw investment state instead,
|
|
250
|
+
/// allowing permission changes to affect liquidity without changing NAV.
|
|
251
|
+
function maxWithdraw(address resource, address holder) external view override returns (uint256) {
|
|
252
|
+
return ICentrifugeAsyncVault(resource).maxWithdraw(holder);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// @inheritdoc IResourceAdapter
|
|
256
|
+
/// @dev Centrifuge publishes no rate on-chain — a fund's return arrives as a new NAV — so the
|
|
257
|
+
/// number comes from the YieldGroup holding the position, where governance publishes it
|
|
258
|
+
/// with {IYieldGroupCentrifuge.setSpotAPYBps}. `YieldGroupBase.spotAPYBps` reaches this by
|
|
259
|
+
/// direct call rather than delegatecall, so `msg.sender` is that group.
|
|
260
|
+
///
|
|
261
|
+
/// Any other caller gets `0`. A published rate belongs to one group's position and this
|
|
262
|
+
/// adapter is shared between groups, so there is no rate to hand someone holding nothing.
|
|
263
|
+
/// Answering rather than reverting keeps the read open to observers, like every other view
|
|
264
|
+
/// here, and that needs both guards below: Solidity checks the callee has code before it
|
|
265
|
+
/// makes the call, and that revert lands outside `try`, so an address with no code has to
|
|
266
|
+
/// be turned away first. The `catch` covers the rest — a contract that is not one of these
|
|
267
|
+
/// groups, which answers nothing this can decode.
|
|
268
|
+
function spotAPYBps(address resource, uint256) external view override returns (uint64) {
|
|
269
|
+
if (msg.sender.code.length == 0) return 0;
|
|
270
|
+
try IYieldGroupCentrifuge(msg.sender).resourceSpotAPYBps(resource) returns (uint64 apyBps) {
|
|
271
|
+
return apyBps;
|
|
272
|
+
} catch {
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// @inheritdoc IResourceAdapter
|
|
278
|
+
/// @dev Reports whether this group still has value tied to the vault by adding its held, pending,
|
|
279
|
+
/// and claimable balances. Asset and share units are mixed intentionally because callers use
|
|
280
|
+
/// the result only as a zero/nonzero check; forced removal logs it as a diagnostic value.
|
|
281
|
+
///
|
|
282
|
+
/// Idle assets are excluded because they are no longer tied to a vault. After redeem or
|
|
283
|
+
/// deposit-cancel proceeds are claimed, this can return zero while those assets remain in the
|
|
284
|
+
/// group. A complete wind-down must also move that idle balance back to the Hub.
|
|
285
|
+
function receiptBalance(address resource, address holder) external view override returns (uint256) {
|
|
286
|
+
ICentrifugeAsyncVault vault = ICentrifugeAsyncVault(resource);
|
|
287
|
+
ICentrifugeAsyncRequestManager.AsyncInvestmentState memory inv = _investments(resource, holder);
|
|
288
|
+
return IERC20(vault.share()).balanceOf(holder) + uint256(inv.maxMint) + inv.maxWithdraw
|
|
289
|
+
+ inv.pendingDepositRequest + inv.pendingRedeemRequest + inv.claimableCancelDepositRequest
|
|
290
|
+
+ inv.claimableCancelRedeemRequest;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// @inheritdoc IResourceAdapter
|
|
294
|
+
/// @dev Verifies the vault exposes the share token, request manager, investment state, and price
|
|
295
|
+
/// reads required by this adapter.
|
|
296
|
+
function validateRegistration(address resource) external view override {
|
|
297
|
+
ICentrifugeAsyncVault vault = ICentrifugeAsyncVault(resource);
|
|
298
|
+
if (vault.share() == address(0)) revert NoShareToken(resource);
|
|
299
|
+
address mgr = vault.baseManager();
|
|
300
|
+
if (mgr == address(0)) revert NoManager(resource);
|
|
301
|
+
ICentrifugeAsyncRequestManager(mgr).investments(resource, address(this));
|
|
302
|
+
vault.pricePerShare();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ============================== Internal — view ===========================
|
|
306
|
+
|
|
307
|
+
/// @dev The vault's share price, or `0` when the read reverts. Swallowing it here is what
|
|
308
|
+
/// turns a broken or unlinked feed into the named {ZeroSharePrice} on a share-holding
|
|
309
|
+
/// position, instead of an opaque revert from inside the vault.
|
|
310
|
+
function _pricePerShareOrZero(address resource) private view returns (uint256) {
|
|
311
|
+
try ICentrifugeAsyncVault(resource).pricePerShare() returns (uint256 price) {
|
|
312
|
+
return price;
|
|
313
|
+
} catch {
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// @dev The asset the calling YieldGroup was initialised with and can never change
|
|
319
|
+
/// (`address(this)` is the group under delegatecall). Deliberately not `resource.asset()`:
|
|
320
|
+
/// the vault is upgradeable and outside Venus control, so if it named another token —
|
|
321
|
+
/// another resource's share token, say — approving or transferring it would hand the vault
|
|
322
|
+
/// the group's balance of that token.
|
|
323
|
+
function _groupAsset() private view returns (IERC20) {
|
|
324
|
+
return IERC20(IYieldGroupBase(address(this)).asset());
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// @dev Reads the position once and hands it back as two subtotals, because its parts are
|
|
328
|
+
/// denominated differently and so price differently:
|
|
329
|
+
/// - `face` is the part already in asset units. It is final; no share price can move it.
|
|
330
|
+
/// - `shares` is the part still measured in shares. A caller must price it before adding.
|
|
331
|
+
/// `shareToken` comes back too, so a caller needing its decimals does not re-read it. The
|
|
332
|
+
/// contract header lists which counter lands in which subtotal.
|
|
333
|
+
function _assetAndShareTotals(address resource, address holder)
|
|
334
|
+
private
|
|
335
|
+
view
|
|
336
|
+
returns (uint256 face, uint256 shares, address shareToken)
|
|
337
|
+
{
|
|
338
|
+
ICentrifugeAsyncRequestManager.AsyncInvestmentState memory inv = _investments(resource, holder);
|
|
339
|
+
|
|
340
|
+
// These counters are denominated in the underlying asset.
|
|
341
|
+
face = uint256(inv.pendingDepositRequest) + inv.claimableCancelDepositRequest + inv.maxWithdraw;
|
|
342
|
+
|
|
343
|
+
// These balances are denominated in shares and must be converted at the vault's NAV.
|
|
344
|
+
shareToken = ICentrifugeAsyncVault(resource).share();
|
|
345
|
+
shares = IERC20(shareToken).balanceOf(holder) + uint256(inv.pendingRedeemRequest) + inv.maxMint
|
|
346
|
+
+ inv.claimableCancelRedeemRequest;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// @dev Reads all pending and claimable balances for a holder from the vault's request manager.
|
|
350
|
+
function _investments(address resource, address holder)
|
|
351
|
+
internal
|
|
352
|
+
view
|
|
353
|
+
returns (ICentrifugeAsyncRequestManager.AsyncInvestmentState memory)
|
|
354
|
+
{
|
|
355
|
+
address manager = ICentrifugeAsyncVault(resource).baseManager();
|
|
356
|
+
return ICentrifugeAsyncRequestManager(manager).investments(resource, holder);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { IResourceAdapter } from "./IResourceAdapter.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title IAdapterCentrifuge
|
|
8
|
+
* @author Venus
|
|
9
|
+
* @notice `IResourceAdapter` plus the Centrifuge-only surface: the async request / cancel / claim
|
|
10
|
+
* primitives, which are delegatecall-only and run in the YieldGroup's context, and the
|
|
11
|
+
* holder-aware `depositRoom` view behind `YieldGroupCentrifuge._resourceDepositRoom`.
|
|
12
|
+
*/
|
|
13
|
+
interface IAdapterCentrifuge is IResourceAdapter {
|
|
14
|
+
// ------------------------- Mutating (delegatecall-only) -------------------
|
|
15
|
+
|
|
16
|
+
/// @notice Open an async redeem request: `shares` move from the group into Centrifuge escrow.
|
|
17
|
+
function requestRedeem(address resource, uint256 shares) external;
|
|
18
|
+
|
|
19
|
+
/// @notice Ask Centrifuge to return the group's pending deposit; blocks new deposit requests
|
|
20
|
+
/// until fulfilled.
|
|
21
|
+
function cancelDepositRequest(address resource) external;
|
|
22
|
+
|
|
23
|
+
/// @notice Ask Centrifuge to return the group's pending redeem shares; blocks new redeem
|
|
24
|
+
/// requests until fulfilled.
|
|
25
|
+
function cancelRedeemRequest(address resource) external;
|
|
26
|
+
|
|
27
|
+
/// @notice Claim all issued shares (`investments().maxMint`) via exact-amount `mint`. Returns
|
|
28
|
+
/// 0 and does nothing when there is nothing to claim.
|
|
29
|
+
function claimDeposit(address resource) external returns (uint256 shares);
|
|
30
|
+
|
|
31
|
+
/// @notice Claim all reserved assets (`investments().maxWithdraw`) via exact-amount
|
|
32
|
+
/// `withdraw`. Returns 0 and does nothing when there is nothing to claim.
|
|
33
|
+
function claimRedeem(address resource) external returns (uint256 assets);
|
|
34
|
+
|
|
35
|
+
/// @notice Collect a fulfilled deposit cancel (`claimableCancelDepositRequest`) as idle assets.
|
|
36
|
+
function claimCancelDeposit(address resource) external returns (uint256 assets);
|
|
37
|
+
|
|
38
|
+
/// @notice Collect a fulfilled redeem cancel (`claimableCancelRedeemRequest`) as held shares.
|
|
39
|
+
function claimCancelRedeem(address resource) external returns (uint256 shares);
|
|
40
|
+
|
|
41
|
+
// --------------------------------- Views ----------------------------------
|
|
42
|
+
|
|
43
|
+
/// @notice Holder-aware deposit headroom: 0 while `holder` has a deposit cancel in flight,
|
|
44
|
+
/// since Centrifuge rejects new requests then; otherwise the uint128 counter headroom.
|
|
45
|
+
/// `IResourceAdapter.maxDeposit` has no holder parameter and cannot express this, so
|
|
46
|
+
/// `YieldGroupCentrifuge._resourceDepositRoom` routes through here instead.
|
|
47
|
+
function depositRoom(address resource, address holder) external view returns (uint256 room);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @notice The vault's current NAV price for one whole share, in asset units.
|
|
51
|
+
* @dev Plain CALL, not delegatecall — it reads no group state. A thin passthrough to
|
|
52
|
+
* `ICentrifugeAsyncVault.pricePerShare`, exposed so callers can read the mark without
|
|
53
|
+
* importing the vault interface itself. Returns the raw reading, `0` included: an unset
|
|
54
|
+
* price only matters when the position actually holds shares, which is
|
|
55
|
+
* {IResourceAdapter.totalAssets}'s call to make, not this one's.
|
|
56
|
+
*
|
|
57
|
+
* A diagnostic rather than part of valuation: `YieldGroupCentrifuge` bounds what the
|
|
58
|
+
* position is worth, so this is the number to read alongside a clamped valuation to see
|
|
59
|
+
* whether the price moved or the share count did.
|
|
60
|
+
* @param resource Centrifuge vault to price.
|
|
61
|
+
* @return price Asset units one whole share converts to at the vault's current NAV.
|
|
62
|
+
*/
|
|
63
|
+
function pricePerShare(address resource) external view returns (uint256 price);
|
|
64
|
+
}
|