@venusprotocol/liquidity-hub 2.1.0-dev.5 → 2.1.0-dev.7
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 +16 -12
- package/contracts/YieldGroup/YieldGroup.sol +1 -1
- package/contracts/YieldGroup/YieldGroupCentrifuge.sol +7 -0
- package/contracts/YieldGroup/YieldGroupFRV.sol +1 -0
- package/contracts/YieldGroup/base/YieldGroupBase.sol +95 -9
- package/contracts/YieldGroup/base/YieldGroupBaseStorage.sol +10 -2
- package/contracts/adapters/AdapterCentrifuge.sol +16 -1
- package/contracts/adapters/AdapterCoreV1.sol +29 -15
- package/contracts/adapters/AdapterFRV.sol +17 -7
- package/contracts/adapters/AdapterFlux.sol +15 -3
- package/contracts/adapters/AdapterSpokeV1.sol +15 -3
- package/contracts/interfaces/IResourceAdapter.sol +12 -3
- package/contracts/interfaces/IYieldGroupBase.sol +56 -7
- package/contracts/interfaces/external/IVToken.sol +9 -0
- package/deployments/bscmainnet/AdapterCoreV1.json +26 -2
- package/deployments/bscmainnet/AdapterFRV.json +26 -2
- package/deployments/bscmainnet/AdapterFlux.json +26 -2
- package/deployments/bscmainnet/AdapterSpokeV1.json +26 -2
- package/deployments/bscmainnet/YieldGroupCoreImpl.json +69 -2
- package/deployments/bscmainnet/YieldGroupFRVImpl.json +69 -2
- package/deployments/bscmainnet/YieldGroupFluxImpl.json +69 -2
- package/deployments/bscmainnet/YieldGroupSpokeImpl.json +69 -2
- package/deployments/bscmainnet_addresses.json +8 -8
- package/deployments/bsctestnet/AdapterCoreV1.json +26 -2
- package/deployments/bsctestnet/AdapterFRV.json +26 -2
- package/deployments/bsctestnet/AdapterFlux.json +26 -2
- package/deployments/bsctestnet/AdapterSpokeV1.json +26 -2
- package/deployments/bsctestnet/YieldGroupCoreImpl.json +69 -2
- package/deployments/bsctestnet/YieldGroupFRVImpl.json +69 -2
- package/deployments/bsctestnet/YieldGroupFluxImpl.json +69 -2
- package/deployments/bsctestnet/YieldGroupSpokeImpl.json +69 -2
- package/deployments/bsctestnet_addresses.json +8 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,9 +195,12 @@ Hub.totalAssets()
|
|
|
195
195
|
= Σ YieldGroup.totalAssets() + Hub idle balance
|
|
196
196
|
│
|
|
197
197
|
└─ YieldGroup.totalAssets()
|
|
198
|
-
= Σ adapter.
|
|
198
|
+
= Σ adapter.receiptValue(resource, trackedReceipt[resource]) + YieldGroup idle balance
|
|
199
|
+
(Centrifuge keeps receipt tracking off and sums adapter.totalAssets(resource, yieldGroup) instead)
|
|
199
200
|
```
|
|
200
201
|
|
|
202
|
+
- **Tracked receipts, not the wallet.** A tracking family prices only the receipts its own deposits minted. Receipt tokens that reach the YieldGroup any other way (a donation) sit in the wallet uncounted: they move neither NAV nor liquidity, never block removal, and are sweepable once the resource is removed. Governance can admit them into the ledger with `syncReceipt`, capped at the amount it reviewed, so receipts sent in while the VIP waits stay uncounted.
|
|
203
|
+
|
|
201
204
|
- **Fails closed.** `totalAssets()` is the share-price denominator, so a YieldGroup whose `totalAssets()` view reverts is **not** isolated here — it halts share math rather than silently dropping that YieldGroup's funds from NAV (which would understate the price, under-pay redeemers and over-credit depositors until the value reappeared). The _quantity views_ (`maxWithdraw`, and `HubLens`'s capacity figure) and the deposit/withdraw routing **do** isolate a faulting YieldGroup (contribute 0 / skip) because failing conservatively there is safe — only on the price would it corrupt the conversion. A view-level revert is a YieldGroup-level bug (resource-level reverts are isolated inside the YieldGroup, so external pauses surface in `withdraw`/`deposit`, not here); recover via the runbook below.
|
|
202
205
|
- Hub idle is normally zero (deposits route out atomically, withdrawals pull exact amounts, reallocate is balanced). A direct token donation persists and is intentionally counted into NAV (it accrues to LPs and is consumed first on withdraw) — the underlying cannot be swept.
|
|
203
206
|
- YieldGroup idle accumulates from treasury-fee gross-up surplus and dust handling on past withdrawals; it is counted and consumed idle-first.
|
|
@@ -333,16 +336,16 @@ room = min( protocolHeadroom, resourceCap − ourBalanceInResource )
|
|
|
333
336
|
|
|
334
337
|
Every privileged call is gated by `AccessControlManagerV8` — the role is `keccak256(targetContract, roleString)`, where `roleString` is the literal function signature shown below. The contracts do not hard-code an operator-vs-governance branch; the asymmetry is entirely a matter of which addresses governance grants each role to. In v1 the Operator is the Venus Core multisig.
|
|
335
338
|
|
|
336
|
-
| Action class
|
|
337
|
-
|
|
|
338
|
-
| Add / remove YieldGroup or Resource, **unpause**, set fees, `updateResourceAdapter`, `sweep` | ✅ | ❌ |
|
|
339
|
-
| **Raise** caps, `emergencyReallocate`
|
|
340
|
-
| **Lower** caps, lower per-tx cap, **pause**, reorder queues
|
|
341
|
-
| `reallocate` between YieldGroups / resources
|
|
342
|
-
| `pauseHub`
|
|
343
|
-
| `deposit` / `mint` / `withdraw` / `redeem`, `accrueFees`, views
|
|
339
|
+
| Action class | Governance (VIP) | Operator |
|
|
340
|
+
| ----------------------------------------------------------------------------------------------------------- | :----------------: | :----------------: |
|
|
341
|
+
| Add / remove YieldGroup or Resource, **unpause**, set fees, `updateResourceAdapter`, `sweep`, `syncReceipt` | ✅ | ❌ |
|
|
342
|
+
| **Raise** caps, `emergencyReallocate` | ✅ (also Guardian) | ❌ |
|
|
343
|
+
| **Lower** caps, lower per-tx cap, **pause**, reorder queues | ✅ | ✅ |
|
|
344
|
+
| `reallocate` between YieldGroups / resources | — | ✅ |
|
|
345
|
+
| `pauseHub` | ✅ | ✅ (also Guardian) |
|
|
346
|
+
| `deposit` / `mint` / `withdraw` / `redeem`, `accrueFees`, views | permissionless | permissionless |
|
|
344
347
|
|
|
345
|
-
**ACM role strings.** Hub: `addYieldGroup(address,uint256,uint16)`, `removeYieldGroup(address)`, `raiseYieldGroupCap(...)`, `lowerYieldGroupCap(...)`, `setOuterDepositQueue(address[])`, `setOuterWithdrawQueue(address[])`, `pauseHub()`, `unpauseHub()`, `pauseYieldGroup(address)`, `unpauseYieldGroup(address)`, `raiseMaxWithdrawalSize(uint256)`, `lowerMaxWithdrawalSize(uint256)`, `setManagementFeeBps(uint16)`, `setPerformanceFeeBps(uint16)`, `setRedeemFeeBps(uint16)`, `setFeeRecipient(address)`, `sweep(address,address)`, `reallocate(...)`, `emergencyReallocate(...)`. Each YieldGroup: `addResource(address,address)`, `removeResource(address)`, `updateResourceAdapter(address,address)` (governance-only, repoints a resource's delegatecall adapter), `setInnerDepositQueue(address[])`, `setInnerWithdrawQueue(address[])`, `pauseResource(address)`, `unpauseResource(address)`, `sweep(address,address)`, and — Core & Flux only — `raiseResourceCap(address,uint256)`, `lowerResourceCap(address,uint256)`, `setBlocksPerYear(uint256)`; — FRV & Centrifuge — `forceRemoveResource(address)`; and — Centrifuge only — `requestRedeem(address,uint256)`, `cancelDepositRequest(address)`, `cancelRedeemRequest(address)`, `claimDeposit(address)`, `claimRedeem(address)`, `claimCancelDeposit(address)`, `claimCancelRedeem(address)`, `setNavGuardRate(address,uint16,uint16,uint16,uint32,bool,bool)`, `setNavGuardSnapshot(address,uint128,uint64)`, `setNavGuardEnabled(address,bool,bool)`, `setSpotAPYBps(address,uint64)`. An onboarding VIP that skips the three `setNavGuard*` roles leaves that resource's NAV band unarmed for good: nothing else can arm it, and an unarmed band reports the counterparty's value to the Hub unclamped.
|
|
348
|
+
**ACM role strings.** Hub: `addYieldGroup(address,uint256,uint16)`, `removeYieldGroup(address)`, `raiseYieldGroupCap(...)`, `lowerYieldGroupCap(...)`, `setOuterDepositQueue(address[])`, `setOuterWithdrawQueue(address[])`, `pauseHub()`, `unpauseHub()`, `pauseYieldGroup(address)`, `unpauseYieldGroup(address)`, `raiseMaxWithdrawalSize(uint256)`, `lowerMaxWithdrawalSize(uint256)`, `setManagementFeeBps(uint16)`, `setPerformanceFeeBps(uint16)`, `setRedeemFeeBps(uint16)`, `setFeeRecipient(address)`, `sweep(address,address)`, `reallocate(...)`, `emergencyReallocate(...)`. Each YieldGroup: `addResource(address,address)`, `removeResource(address)`, `updateResourceAdapter(address,address)` (governance-only, repoints a resource's delegatecall adapter), `setInnerDepositQueue(address[])`, `setInnerWithdrawQueue(address[])`, `pauseResource(address)`, `unpauseResource(address)`, `sweep(address,address)`, `syncReceipt(address,uint256)`, and — Core & Flux only — `raiseResourceCap(address,uint256)`, `lowerResourceCap(address,uint256)`, `setBlocksPerYear(uint256)`; — FRV & Centrifuge — `forceRemoveResource(address)`; and — Centrifuge only — `requestRedeem(address,uint256)`, `cancelDepositRequest(address)`, `cancelRedeemRequest(address)`, `claimDeposit(address)`, `claimRedeem(address)`, `claimCancelDeposit(address)`, `claimCancelRedeem(address)`, `setNavGuardRate(address,uint16,uint16,uint16,uint32,bool,bool)`, `setNavGuardSnapshot(address,uint128,uint64)`, `setNavGuardEnabled(address,bool,bool)`, `setSpotAPYBps(address,uint64)`. An onboarding VIP that skips the three `setNavGuard*` roles leaves that resource's NAV band unarmed for good: nothing else can arm it, and an unarmed band reports the counterparty's value to the Hub unclamped.
|
|
346
349
|
|
|
347
350
|
## Invariants & safety
|
|
348
351
|
|
|
@@ -350,7 +353,7 @@ Every privileged call is gated by `AccessControlManagerV8` — the role is `kecc
|
|
|
350
353
|
- **Net-zero reallocate.** `Σ withdraws == Σ deposits`; the Operator can only move funds among registered routes, never in or out.
|
|
351
354
|
- **Stateless adapters.** Adapters declare zero storage (only immutables), never `sstore`, and call only the resource, its `underlying()`, and its `comptroller()`. Mutating entry points revert outside a delegatecall context.
|
|
352
355
|
- **Fault isolation.** A YieldGroup with a reverting `totalAssets()` view contributes 0 instead of bricking the Hub; it can still be removed as an emergency eviction.
|
|
353
|
-
- **Removal safety.** `removeResource` / `removeYieldGroup` gate on
|
|
356
|
+
- **Removal safety.** `removeResource` / `removeYieldGroup` gate on receipts the YieldGroup accounts for, in _receipt-token units_ (not underlying value) so a sub-unit residual can't be rounded to zero and orphan tokens. For a tracking family that is the tracked ledger, so donated receipts never block removal and are sweepable afterwards; for Centrifuge it is the wallet balance.
|
|
354
357
|
- **Inflation defense.** ERC-4626 decimals offset (≤ 12) is set per asset at init; the deploy path is responsible for a non-zero value.
|
|
355
358
|
- **Standard ERC-20 only.** Fee-on-transfer, deflationary, and rebasing underlyings are unsupported (deposits fail closed if a token delivers less than requested).
|
|
356
359
|
- **Upgrade-safe storage.** Each contract isolates its state in a `*Storage` base with a reserved `__gap`; never reorder or prepend fields.
|
|
@@ -370,7 +373,7 @@ contracts/
|
|
|
370
373
|
migrator/ Migrator.sol (immutable Core→Hub one-click migration)
|
|
371
374
|
registry/ HubRegistry.sol (chain-level Hub directory, discovery root)
|
|
372
375
|
interfaces/ IYieldGroupBase, IResourceAdapter, IHub, IHubLens, IHubRegistry, IYieldGroup*, IMigrator, IVToken, IVBep20, IWBNB, IFToken, IFRVVault, ...
|
|
373
|
-
deploy/ Foundry deploy scripts 01–
|
|
376
|
+
deploy/ Foundry deploy scripts 01–15 + DeployBase + config/<network>.json
|
|
374
377
|
tests/foundry/ unit (Hub/, YieldGroup/{core,flux,frv}/, Registry/, lens/), Integration/, fork/
|
|
375
378
|
deployments/<network>/ committed registry of deployed addresses (source of truth)
|
|
376
379
|
lib/ git-submodule dependencies
|
|
@@ -441,6 +444,7 @@ Beacon-proxy model: one `UpgradeableBeacon` **per family per chain** (Hub, Core,
|
|
|
441
444
|
12_DeployHubImpl # upgrade only: new Hub impl behind the existing HubBeacon
|
|
442
445
|
13_DeploySpoke # once per chain (Spoke adapter + impl + beacon)
|
|
443
446
|
14_DeploySpokeSource # per asset, backfills existing Hubs
|
|
447
|
+
15_DeployReceiptLedger # upgrade only: receipt-ledger YieldGroup impls + the four tracking adapters
|
|
444
448
|
```
|
|
445
449
|
|
|
446
450
|
Scripts run in numeric order: beacons first, then the registry, then per-asset instances against both. Scripts only deploy + initialize — **ACM wiring, `addYieldGroup` / `addResource`, and queue configuration are governance (ACM-gated) actions done separately.**
|
|
@@ -110,7 +110,7 @@ contract YieldGroup is YieldGroupBase, IYieldGroup {
|
|
|
110
110
|
uint256 cap = _resourceCap[resource];
|
|
111
111
|
if (cap == 0) return protocolRoom;
|
|
112
112
|
|
|
113
|
-
uint256 held =
|
|
113
|
+
uint256 held = _accountedAssetValue(resource, adapter);
|
|
114
114
|
uint256 operatorRoom = cap > held ? cap - held : 0;
|
|
115
115
|
return protocolRoom < operatorRoom ? protocolRoom : operatorRoom;
|
|
116
116
|
}
|
|
@@ -364,6 +364,13 @@ contract YieldGroupCentrifuge is YieldGroupBase, NavGuard, IYieldGroupCentrifuge
|
|
|
364
364
|
|
|
365
365
|
// ============================== Internal — hooks ==========================
|
|
366
366
|
|
|
367
|
+
/// @dev Off for this family: Centrifuge mints the shares when it settles a request, outside our
|
|
368
|
+
/// deposit call, so the base's balance delta around the dispatch would never see them. The
|
|
369
|
+
/// NavGuard band bounds this family's valuation instead.
|
|
370
|
+
function _tracksReceipts() internal pure override returns (bool) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
367
374
|
/// @dev Unlike `IResourceAdapter.maxDeposit`, `depositRoom` can inspect this YieldGroup's state
|
|
368
375
|
/// for the vault. It returns zero while this group has a deposit cancellation pending, so
|
|
369
376
|
/// the Hub does not advertise capacity that Centrifuge would reject.
|
|
@@ -83,6 +83,7 @@ contract YieldGroupFRV is YieldGroupBase, IYieldGroupFRV {
|
|
|
83
83
|
_removeFromQueue(_innerDepositQueue, resource);
|
|
84
84
|
_removeFromQueue(_innerWithdrawQueue, resource);
|
|
85
85
|
|
|
86
|
+
delete _trackedReceipt[resource];
|
|
86
87
|
delete _resourcesConfig[resource];
|
|
87
88
|
|
|
88
89
|
emit ResourceForceRemoved(resource, orphaned);
|
|
@@ -140,9 +140,9 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
140
140
|
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
141
141
|
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
142
142
|
|
|
143
|
-
// Gate on the
|
|
144
|
-
//
|
|
145
|
-
uint256 held =
|
|
143
|
+
// Gate on receipts the YieldGroup accounts for, not the wallet. Donated receipts were never counted and cannot
|
|
144
|
+
// be withdrawn, so they would pin a drained resource here; once unregistered, `sweep` recovers them.
|
|
145
|
+
uint256 held = _accountedReceipts(resource, cfg.adapter);
|
|
146
146
|
if (held != 0) revert ResourceHasBalance(resource, held);
|
|
147
147
|
|
|
148
148
|
uint256 idxPlusOne = _resourceIndex[resource];
|
|
@@ -163,6 +163,7 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
163
163
|
// same address silently inherit a stale value, since `addResource` never writes it and the
|
|
164
164
|
// setter is registered-only. Removal restores a clean slate.
|
|
165
165
|
delete _resourceCap[resource];
|
|
166
|
+
delete _trackedReceipt[resource];
|
|
166
167
|
delete _resourcesConfig[resource];
|
|
167
168
|
_onResourceRemoved(resource);
|
|
168
169
|
|
|
@@ -255,6 +256,24 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
/// @inheritdoc IYieldGroupBase
|
|
260
|
+
/// @dev This is the only way a receipt-token donation reaches the Hub's share price, so it is
|
|
261
|
+
/// governance deciding the tokens are worth what the resource says. A vToken of a market
|
|
262
|
+
/// carrying unsocialised bad debt is not, and syncing it hands that loss to the Hub's LPs.
|
|
263
|
+
function syncReceipt(address resource, uint256 maxAdded) external override {
|
|
264
|
+
_checkAccessAllowed("syncReceipt(address,uint256)");
|
|
265
|
+
if (!_tracksReceipts()) revert ReceiptTrackingOff();
|
|
266
|
+
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
267
|
+
if (!cfg.registered) revert ResourceNotRegistered(resource);
|
|
268
|
+
|
|
269
|
+
uint256 held = IResourceAdapter(cfg.adapter).receiptBalance(resource, address(this));
|
|
270
|
+
uint256 old = _trackedReceipt[resource];
|
|
271
|
+
uint256 synced = held;
|
|
272
|
+
if (held > old && held - old > maxAdded) synced = old + maxAdded;
|
|
273
|
+
_trackedReceipt[resource] = synced;
|
|
274
|
+
emit ReceiptSynced(resource, old, synced);
|
|
275
|
+
}
|
|
276
|
+
|
|
258
277
|
// -- IYieldGroupBase: deposit / withdraw --
|
|
259
278
|
|
|
260
279
|
/// @inheritdoc IYieldGroupBase
|
|
@@ -367,7 +386,7 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
367
386
|
_prepareResource(resource);
|
|
368
387
|
|
|
369
388
|
address adapter = cfg.adapter;
|
|
370
|
-
uint256 liquid =
|
|
389
|
+
uint256 liquid = _liquidAssetValue(resource, adapter);
|
|
371
390
|
if (amount > liquid) revert ResourceLiquidityInsufficient(amount, liquid);
|
|
372
391
|
|
|
373
392
|
uint256 received = _dispatchWithdraw(adapter, resource, amount, to);
|
|
@@ -402,6 +421,11 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
402
421
|
return (cfg.registered, cfg.paused, cfg.adapter);
|
|
403
422
|
}
|
|
404
423
|
|
|
424
|
+
/// @inheritdoc IYieldGroupBase
|
|
425
|
+
function trackedReceipt(address resource) external view override returns (uint256) {
|
|
426
|
+
return _trackedReceipt[resource];
|
|
427
|
+
}
|
|
428
|
+
|
|
405
429
|
/// @inheritdoc IYieldGroupBase
|
|
406
430
|
function innerDepositQueue() external view override returns (address[] memory) {
|
|
407
431
|
return _innerDepositQueue;
|
|
@@ -429,7 +453,7 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
429
453
|
uint256 len = list.length;
|
|
430
454
|
for (uint256 i; i < len; ++i) {
|
|
431
455
|
address resource = list[i];
|
|
432
|
-
total +=
|
|
456
|
+
total += _accountedAssetValue(resource, _resourcesConfig[resource].adapter);
|
|
433
457
|
}
|
|
434
458
|
total += IERC20(_assetToken).balanceOf(address(this));
|
|
435
459
|
}
|
|
@@ -465,7 +489,7 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
465
489
|
address resource = queue[i];
|
|
466
490
|
ResourceConfig storage cfg = _resourcesConfig[resource];
|
|
467
491
|
if (cfg.registered && !cfg.paused) {
|
|
468
|
-
total +=
|
|
492
|
+
total += _liquidAssetValue(resource, cfg.adapter);
|
|
469
493
|
}
|
|
470
494
|
}
|
|
471
495
|
total += IERC20(_assetToken).balanceOf(address(this));
|
|
@@ -484,7 +508,7 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
484
508
|
for (uint256 i; i < len; ++i) {
|
|
485
509
|
address resource = list[i];
|
|
486
510
|
address adapter = _resourcesConfig[resource].adapter;
|
|
487
|
-
uint256 resVal =
|
|
511
|
+
uint256 resVal = _accountedAssetValue(resource, adapter);
|
|
488
512
|
if (resVal > 0) {
|
|
489
513
|
uint256 apy = IResourceAdapter(adapter).spotAPYBps(resource, bpy);
|
|
490
514
|
weighted += apy * resVal;
|
|
@@ -579,7 +603,7 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
579
603
|
_prepareResource(resource);
|
|
580
604
|
|
|
581
605
|
address adapter = cfg.adapter;
|
|
582
|
-
uint256 liquid =
|
|
606
|
+
uint256 liquid = _liquidAssetValue(resource, adapter);
|
|
583
607
|
uint256 toDeliver = remaining < liquid ? remaining : liquid;
|
|
584
608
|
if (toDeliver == 0) return 0;
|
|
585
609
|
|
|
@@ -626,7 +650,10 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
626
650
|
|
|
627
651
|
/// @custom:oz-upgrades-unsafe-allow-reachable delegatecall
|
|
628
652
|
function _dispatchDeposit(address adapter, address resource, uint256 amount) internal virtual {
|
|
653
|
+
bool tracks = _tracksReceipts();
|
|
654
|
+
uint256 before = tracks ? _totalReceipts(resource, adapter) : 0;
|
|
629
655
|
adapter.functionDelegateCall(abi.encodeCall(IResourceAdapter.deposit, (resource, amount)));
|
|
656
|
+
if (tracks) _trackedReceipt[resource] += _totalReceipts(resource, adapter) - before;
|
|
630
657
|
emit DepositRouted(resource, amount);
|
|
631
658
|
}
|
|
632
659
|
|
|
@@ -657,9 +684,26 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
657
684
|
{
|
|
658
685
|
IERC20 assetToken = IERC20(_assetToken);
|
|
659
686
|
uint256 preBalTo = assetToken.balanceOf(to);
|
|
687
|
+
bool tracks = _tracksReceipts();
|
|
688
|
+
uint256 preHeld = tracks ? _totalReceipts(resource, adapter) : 0;
|
|
660
689
|
adapter.functionDelegateCall(abi.encodeCall(IResourceAdapter.withdraw, (resource, amount, to)));
|
|
661
690
|
received = assetToken.balanceOf(to) - preBalTo;
|
|
662
691
|
|
|
692
|
+
// The vault burns from the wallet, which can hold receipts the ledger never counted. Callers
|
|
693
|
+
// size `amount` against the tracked value, so the burn stays within the tracked count unless
|
|
694
|
+
// the adapter burns more than asked: Spoke sweeps a worthless remainder whole, donated dust
|
|
695
|
+
// included. Floor at zero rather than revert, or that dust would block the withdrawal.
|
|
696
|
+
if (tracks) {
|
|
697
|
+
uint256 burned = preHeld - _totalReceipts(resource, adapter);
|
|
698
|
+
uint256 tracked = _trackedReceipt[resource];
|
|
699
|
+
uint256 left = burned < tracked ? tracked - burned : 0;
|
|
700
|
+
// Spoke decides whether to burn a worthless remainder by looking at the wallet, so a donation
|
|
701
|
+
// can leave receipts counted here that are worth 0. No withdrawal reaches them and they would
|
|
702
|
+
// block `removeResource`, so drop them; `sweep` recovers them from the wallet after removal.
|
|
703
|
+
if (left != 0 && IResourceAdapter(adapter).receiptValue(resource, left) == 0) left = 0;
|
|
704
|
+
_trackedReceipt[resource] = left;
|
|
705
|
+
}
|
|
706
|
+
|
|
663
707
|
emit WithdrawRouted(resource, amount);
|
|
664
708
|
}
|
|
665
709
|
|
|
@@ -749,7 +793,7 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
749
793
|
uint256 len = list.length;
|
|
750
794
|
for (uint256 i; i < len; ++i) {
|
|
751
795
|
address resource = list[i];
|
|
752
|
-
uint256 held =
|
|
796
|
+
uint256 held = _accountedReceipts(resource, _resourcesConfig[resource].adapter);
|
|
753
797
|
if (held != 0 && !_queueContains(queue, resource)) {
|
|
754
798
|
revert WithdrawQueueOmitsFundedResource(resource);
|
|
755
799
|
}
|
|
@@ -816,4 +860,46 @@ abstract contract YieldGroupBase is ReentrancyGuardUpgradeable, YieldGroupBaseSt
|
|
|
816
860
|
function _resourceDepositRoom(address resource, address adapter) internal view virtual returns (uint256 room) {
|
|
817
861
|
return IResourceAdapter(adapter).maxDeposit(resource);
|
|
818
862
|
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* @notice Whether valuation counts only the receipt tokens this YieldGroup minted itself.
|
|
866
|
+
* @dev Base default: true. Holds for every family whose receipt balance moves only when this
|
|
867
|
+
* contract deposits or redeems (Core, Spoke, Flux, FRV). `YieldGroupCentrifuge` overrides to
|
|
868
|
+
* false: its shares are minted by Centrifuge when it settles a request, not inside our
|
|
869
|
+
* deposit call, so a balance delta measured around the dispatch would miss them.
|
|
870
|
+
* @return tracks True to price `_trackedReceipt` through the adapter's `receiptValue` instead of
|
|
871
|
+
* its holder-wide `totalAssets`.
|
|
872
|
+
*/
|
|
873
|
+
function _tracksReceipts() internal view virtual returns (bool tracks) {
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/// @dev Value of our position in `resource`: the tracked receipts priced by the adapter, or the
|
|
878
|
+
/// whole wallet when this family does not track.
|
|
879
|
+
function _accountedAssetValue(address resource, address adapter) internal view returns (uint256) {
|
|
880
|
+
if (_tracksReceipts()) return IResourceAdapter(adapter).receiptValue(resource, _trackedReceipt[resource]);
|
|
881
|
+
return IResourceAdapter(adapter).totalAssets(resource, address(this));
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/// @dev Withdrawable underlying from `resource` right now. The adapter's figure is what the vault
|
|
885
|
+
/// will pay this wallet, so it is capped at what our tracked receipts are worth; untracked
|
|
886
|
+
/// receipts never certify liquidity.
|
|
887
|
+
function _liquidAssetValue(address resource, address adapter) private view returns (uint256) {
|
|
888
|
+
uint256 liquid = IResourceAdapter(adapter).maxWithdraw(resource, address(this));
|
|
889
|
+
if (!_tracksReceipts()) return liquid;
|
|
890
|
+
uint256 worth = _accountedAssetValue(resource, adapter);
|
|
891
|
+
return worth < liquid ? worth : liquid;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/// @dev Every receipt in the wallet, counted or not.
|
|
895
|
+
function _totalReceipts(address resource, address adapter) private view returns (uint256) {
|
|
896
|
+
return IResourceAdapter(adapter).receiptBalance(resource, address(this));
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/// @dev Receipts the Hub accounts for: the tracked ledger when the family tracks, the wallet otherwise.
|
|
900
|
+
/// The registry gates read this so they agree with valuation on what "funded" means.
|
|
901
|
+
function _accountedReceipts(address resource, address adapter) private view returns (uint256) {
|
|
902
|
+
if (_tracksReceipts()) return _trackedReceipt[resource];
|
|
903
|
+
return _totalReceipts(resource, adapter);
|
|
904
|
+
}
|
|
819
905
|
}
|
|
@@ -29,7 +29,8 @@ import { IYieldGroupBase } from "../../interfaces/IYieldGroupBase.sol";
|
|
|
29
29
|
* 7 32 _innerDepositQueue
|
|
30
30
|
* 8 32 _innerWithdrawQueue
|
|
31
31
|
* 9 32 _resourceCap (generic only; never written by FRV)
|
|
32
|
-
*
|
|
32
|
+
* 10 32 _trackedReceipt
|
|
33
|
+
* 11..48 32 × 38 __gap
|
|
33
34
|
*/
|
|
34
35
|
abstract contract YieldGroupBaseStorage {
|
|
35
36
|
/// @notice Hub authorised to call `deposit` / `withdraw`. Set at initialisation.
|
|
@@ -67,7 +68,14 @@ abstract contract YieldGroupBaseStorage {
|
|
|
67
68
|
/// generic `YieldGroup` reads / writes this; FRV leaves it `0`.
|
|
68
69
|
mapping(address resource => uint256 cap) internal _resourceCap;
|
|
69
70
|
|
|
71
|
+
/// @notice Receipt tokens (vTokens, vault shares) this YieldGroup minted itself, per resource.
|
|
72
|
+
/// Valuation counts these and ignores any surplus the resource's `balanceOf` shows, so
|
|
73
|
+
/// receipt tokens sent here directly do not move the Hub's share price. Written only by
|
|
74
|
+
/// the deposit / withdraw dispatch and `syncReceipt`, and cleared on removal; families
|
|
75
|
+
/// whose receipts arrive asynchronously (Centrifuge) leave it unused.
|
|
76
|
+
mapping(address resource => uint256 shares) internal _trackedReceipt;
|
|
77
|
+
|
|
70
78
|
/// @dev Reserved slots for future state. Shrink by the number of slots a new field
|
|
71
79
|
/// consumes when extending.
|
|
72
|
-
uint256[
|
|
80
|
+
uint256[38] private __gap;
|
|
73
81
|
}
|
|
@@ -81,6 +81,15 @@ contract AdapterCentrifuge is IAdapterCentrifuge {
|
|
|
81
81
|
*/
|
|
82
82
|
error ZeroSharePrice(address resource);
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @notice Thrown by {receiptValue}, which this family implements only to satisfy `IResourceAdapter`.
|
|
86
|
+
* @dev `YieldGroupCentrifuge` turns receipt tracking off because deposits and withdrawals are
|
|
87
|
+
* asynchronous: Centrifuge mints the shares at settlement, after our deposit call has
|
|
88
|
+
* returned, so the base never calls {receiptValue} here.
|
|
89
|
+
* @param resource Resource that was queried.
|
|
90
|
+
*/
|
|
91
|
+
error ShareCountNotPriceable(address resource);
|
|
92
|
+
|
|
84
93
|
// ============================== Modifiers =================================
|
|
85
94
|
|
|
86
95
|
/// @dev Restricts mutating adapter functions to delegatecall execution.
|
|
@@ -211,7 +220,8 @@ contract AdapterCentrifuge is IAdapterCentrifuge {
|
|
|
211
220
|
/// `totalAssets()` for the figure the Hub sums, and this one for what the band is clamping.
|
|
212
221
|
///
|
|
213
222
|
/// Only the share-denominated buckets need a price, so a position holding none is valued
|
|
214
|
-
/// without the feed at all.
|
|
223
|
+
/// without the feed at all. Prices everything `holder` has at the vault; this family keeps
|
|
224
|
+
/// receipt tracking off, so there is no tracked figure.
|
|
215
225
|
function totalAssets(address resource, address holder) external view override returns (uint256 value) {
|
|
216
226
|
(uint256 face, uint256 shares, address shareToken) = _assetAndShareTotals(resource, holder);
|
|
217
227
|
value = face;
|
|
@@ -252,6 +262,11 @@ contract AdapterCentrifuge is IAdapterCentrifuge {
|
|
|
252
262
|
return ICentrifugeAsyncVault(resource).maxWithdraw(holder);
|
|
253
263
|
}
|
|
254
264
|
|
|
265
|
+
/// @inheritdoc IResourceAdapter
|
|
266
|
+
function receiptValue(address resource, uint256) external pure override returns (uint256) {
|
|
267
|
+
revert ShareCountNotPriceable(resource);
|
|
268
|
+
}
|
|
269
|
+
|
|
255
270
|
/// @inheritdoc IResourceAdapter
|
|
256
271
|
/// @dev Centrifuge publishes no rate on-chain — a fund's return arrives as a new NAV — so the
|
|
257
272
|
/// number comes from the YieldGroup holding the position, where governance publishes it
|
|
@@ -166,20 +166,17 @@ contract AdapterCoreV1 is IResourceAdapter {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/// @inheritdoc IResourceAdapter
|
|
169
|
-
/// @dev
|
|
170
|
-
///
|
|
171
|
-
///
|
|
172
|
-
///
|
|
173
|
-
/// (every registered market today) this returns the gross value unchanged.
|
|
169
|
+
/// @dev Priced net of the Comptroller's treasury exit fee, because `withdraw` burns extra vTokens
|
|
170
|
+
/// to cover it. Valuing gross would overstate NAV and let early withdrawers leave the fee to
|
|
171
|
+
/// the last ones out. With the fee at zero this is the plain stored-rate value.
|
|
172
|
+
/// Prices everything `holder` has in the wallet, not the YieldGroup's tracked receipts.
|
|
174
173
|
function totalAssets(address resource, address holder) external view override returns (uint256) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
uint256 gross = (vBal * IVToken(resource).exchangeRateStored()) / EXP_SCALE;
|
|
174
|
+
return _receiptValue(resource, IVToken(resource).balanceOf(holder));
|
|
175
|
+
}
|
|
178
176
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return (gross * (MANTISSA_ONE - treasuryPct)) / MANTISSA_ONE;
|
|
177
|
+
/// @inheritdoc IResourceAdapter
|
|
178
|
+
function receiptValue(address resource, uint256 vBal) external view override returns (uint256) {
|
|
179
|
+
return _receiptValue(resource, vBal);
|
|
183
180
|
}
|
|
184
181
|
|
|
185
182
|
/// @inheritdoc IResourceAdapter
|
|
@@ -206,9 +203,9 @@ contract AdapterCoreV1 is IResourceAdapter {
|
|
|
206
203
|
|
|
207
204
|
/// @inheritdoc IResourceAdapter
|
|
208
205
|
/// @dev Three constraints: (a) what `holder` holds in the resource (vToken balance × ER),
|
|
209
|
-
/// (b) what the vToken can pay out in cash (`getCash`), and (c)
|
|
210
|
-
/// (`treasuryPercent`). The min of (a) and (b) is the gross redeemable;
|
|
211
|
-
/// by `(1 − treasuryPct)` reflects what actually arrives at the recipient.
|
|
206
|
+
/// (b) what the vToken can pay out in cash (`getCash` net of `totalReserves`), and (c)
|
|
207
|
+
/// the treasury cut (`treasuryPercent`). The min of (a) and (b) is the gross redeemable;
|
|
208
|
+
/// multiplying by `(1 − treasuryPct)` reflects what actually arrives at the recipient.
|
|
212
209
|
function maxWithdraw(address resource, address holder) external view override returns (uint256) {
|
|
213
210
|
IComptroller comptrollerContract = IComptroller(IVToken(resource).comptroller());
|
|
214
211
|
// Action.REDEEM == 1 in the Venus Comptroller's enum.
|
|
@@ -221,6 +218,10 @@ contract AdapterCoreV1 is IResourceAdapter {
|
|
|
221
218
|
if (ourValue == 0) return 0;
|
|
222
219
|
|
|
223
220
|
uint256 cash = IVToken(resource).getCash();
|
|
221
|
+
// `accrueInterest` sends the reserves out of cash when a reduction is due.
|
|
222
|
+
uint256 reserves = IVToken(resource).totalReserves();
|
|
223
|
+
if (cash <= reserves) return 0;
|
|
224
|
+
cash -= reserves;
|
|
224
225
|
// A redeem that would burn 0 vTokens is bumped to one whole vToken unit of underlying by
|
|
225
226
|
// `_floorRedeemToOneVToken`. If the market's cash is below that unit, every non-zero redeem
|
|
226
227
|
// the cascade could try exceeds cash and reverts in Venus `redeemFresh`. Report 0 liquidity
|
|
@@ -264,6 +265,19 @@ contract AdapterCoreV1 is IResourceAdapter {
|
|
|
264
265
|
if (treasuryPct != 0) revert TreasuryFeeUnsupported(resource, treasuryPct);
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
// ============================== Internal — view ==========================
|
|
269
|
+
|
|
270
|
+
/// @dev Realizable value of `vBal` vTokens: stored-rate gross, net of the Comptroller treasury cut.
|
|
271
|
+
function _receiptValue(address resource, uint256 vBal) internal view returns (uint256) {
|
|
272
|
+
if (vBal == 0) return 0;
|
|
273
|
+
uint256 gross = (vBal * IVToken(resource).exchangeRateStored()) / EXP_SCALE;
|
|
274
|
+
|
|
275
|
+
uint256 treasuryPct = IComptroller(IVToken(resource).comptroller()).treasuryPercent();
|
|
276
|
+
if (treasuryPct == 0) return gross;
|
|
277
|
+
// The Comptroller setter enforces `treasuryPercent < 1e18`, so the subtraction cannot underflow.
|
|
278
|
+
return (gross * (MANTISSA_ONE - treasuryPct)) / MANTISSA_ONE;
|
|
279
|
+
}
|
|
280
|
+
|
|
267
281
|
// ============================== Private — view ===========================
|
|
268
282
|
|
|
269
283
|
/**
|
|
@@ -120,11 +120,16 @@ contract AdapterFRV is IResourceAdapter {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/// @inheritdoc IResourceAdapter
|
|
123
|
-
/// @dev Position value = principal slice + accrued-coupon slice
|
|
124
|
-
/// the
|
|
123
|
+
/// @dev Position value = principal slice + accrued-coupon slice. See the contract NatSpec for
|
|
124
|
+
/// the valuation model. Prices everything `holder` has in the wallet, not the YieldGroup's
|
|
125
|
+
/// tracked receipts.
|
|
125
126
|
function totalAssets(address resource, address holder) external view override returns (uint256) {
|
|
126
|
-
(
|
|
127
|
-
|
|
127
|
+
return _receiptValue(resource, IFRVVault(resource).balanceOf(holder));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// @inheritdoc IResourceAdapter
|
|
131
|
+
function receiptValue(address resource, uint256 shares) external view override returns (uint256) {
|
|
132
|
+
return _receiptValue(resource, shares);
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
/// @inheritdoc IResourceAdapter
|
|
@@ -208,7 +213,13 @@ contract AdapterFRV is IResourceAdapter {
|
|
|
208
213
|
|
|
209
214
|
// ============================== Internal — view ==========================
|
|
210
215
|
|
|
211
|
-
/// @dev
|
|
216
|
+
/// @dev Principal slice plus accrued-coupon slice of `shares`, in underlying.
|
|
217
|
+
function _receiptValue(address resource, uint256 shares) internal view returns (uint256) {
|
|
218
|
+
(uint256 principalValue, uint256 couponValueGross) = _breakdown(resource, shares);
|
|
219
|
+
return principalValue + couponValueGross;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// @dev Split `shares` of the vault into (principalValue, couponValueGross), both in underlying.
|
|
212
223
|
/// Branches by lifecycle state:
|
|
213
224
|
/// - no shares -> (0, 0)
|
|
214
225
|
/// - Closed -> (0, 0) (stuck residual; valued at 0 = deliverable value)
|
|
@@ -217,13 +228,12 @@ contract AdapterFRV is IResourceAdapter {
|
|
|
217
228
|
/// Both slices use the vault's own +1/+1 floored ERC-4626 basis (decimalsOffset 0), so the
|
|
218
229
|
/// principal slice equals `previewRedeem(shares)` during non-terminal states and the gross
|
|
219
230
|
/// sum equals the post-Matured `previewRedeem` at `elapsed == lockDuration`.
|
|
220
|
-
function _breakdown(address resource,
|
|
231
|
+
function _breakdown(address resource, uint256 shares)
|
|
221
232
|
internal
|
|
222
233
|
view
|
|
223
234
|
returns (uint256 principalValue, uint256 couponValueGross)
|
|
224
235
|
{
|
|
225
236
|
IFRVVault vault = IFRVVault(resource);
|
|
226
|
-
uint256 shares = vault.balanceOf(holder);
|
|
227
237
|
if (shares == 0) return (0, 0);
|
|
228
238
|
|
|
229
239
|
IFRVVault.FRVVaultRuntime memory rt = vault.runtime();
|
|
@@ -112,10 +112,14 @@ contract AdapterFlux is IResourceAdapter {
|
|
|
112
112
|
/// @inheritdoc IResourceAdapter
|
|
113
113
|
/// @dev `previewRedeem(balanceOf(holder))` — the per-holder position value (shares × token
|
|
114
114
|
/// exchange price, including accrued rewards).
|
|
115
|
+
/// Prices everything `holder` has in the wallet, not the YieldGroup's tracked receipts.
|
|
115
116
|
function totalAssets(address resource, address holder) external view override returns (uint256) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
return _receiptValue(resource, IFToken(resource).balanceOf(holder));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// @inheritdoc IResourceAdapter
|
|
121
|
+
function receiptValue(address resource, uint256 shares) external view override returns (uint256) {
|
|
122
|
+
return _receiptValue(resource, shares);
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
/// @inheritdoc IResourceAdapter
|
|
@@ -176,4 +180,12 @@ contract AdapterFlux is IResourceAdapter {
|
|
|
176
180
|
// YieldGroup's `asset()` probe and `code.length` check at registration time.
|
|
177
181
|
resource;
|
|
178
182
|
}
|
|
183
|
+
|
|
184
|
+
// ============================== Internal — view ==========================
|
|
185
|
+
|
|
186
|
+
/// @dev `previewRedeem(shares)`: shares at the token's exchange price, accrued rewards included.
|
|
187
|
+
function _receiptValue(address resource, uint256 shares) internal view returns (uint256) {
|
|
188
|
+
if (shares == 0) return 0;
|
|
189
|
+
return IFToken(resource).previewRedeem(shares);
|
|
190
|
+
}
|
|
179
191
|
}
|
|
@@ -245,10 +245,14 @@ contract AdapterSpokeV1 is IResourceAdapter {
|
|
|
245
245
|
/// @dev `balance x exchangeRateStored`: the market's own valuation of the tokens, and what
|
|
246
246
|
/// redeeming all of them would pay at the stored rate if the market had the cash. Includes
|
|
247
247
|
/// the position's share of `badDebt`; see the contract NatSpec.
|
|
248
|
+
/// Prices everything `holder` has in the wallet, not the YieldGroup's tracked receipts.
|
|
248
249
|
function totalAssets(address resource, address holder) external view override returns (uint256) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
return _receiptValue(resource, IVTokenIsolated(resource).balanceOf(holder));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// @inheritdoc IResourceAdapter
|
|
254
|
+
function receiptValue(address resource, uint256 vBal) external view override returns (uint256) {
|
|
255
|
+
return _receiptValue(resource, vBal);
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
/// @inheritdoc IResourceAdapter
|
|
@@ -396,6 +400,14 @@ contract AdapterSpokeV1 is IResourceAdapter {
|
|
|
396
400
|
}
|
|
397
401
|
}
|
|
398
402
|
|
|
403
|
+
// ============================== Internal — view ==========================
|
|
404
|
+
|
|
405
|
+
/// @dev `vBal` vTokens at the market's stored exchange rate.
|
|
406
|
+
function _receiptValue(address resource, uint256 vBal) internal view returns (uint256) {
|
|
407
|
+
if (vBal == 0) return 0;
|
|
408
|
+
return (vBal * IVTokenIsolated(resource).exchangeRateStored()) / EXP_SCALE;
|
|
409
|
+
}
|
|
410
|
+
|
|
399
411
|
// ============================== Private — pure ===========================
|
|
400
412
|
|
|
401
413
|
/**
|
|
@@ -103,6 +103,16 @@ interface IResourceAdapter {
|
|
|
103
103
|
*/
|
|
104
104
|
function totalAssets(address resource, address holder) external view returns (uint256 value);
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* @notice Underlying value of `shares` receipt tokens of `resource`, priced exactly as
|
|
108
|
+
* {totalAssets} prices a holder's balance. Lets the YieldGroup value the receipts it
|
|
109
|
+
* minted itself rather than everything its wallet happens to hold.
|
|
110
|
+
* @param resource Underlying yield protocol address.
|
|
111
|
+
* @param shares Receipt-token amount to price, in the resource's own units.
|
|
112
|
+
* @return value Underlying units `shares` are worth.
|
|
113
|
+
*/
|
|
114
|
+
function receiptValue(address resource, uint256 shares) external view returns (uint256 value);
|
|
115
|
+
|
|
106
116
|
/**
|
|
107
117
|
* @notice Spare deposit headroom on `resource` right now (capacity remaining before the
|
|
108
118
|
* resource rejects mint via its own caps or pause).
|
|
@@ -137,9 +147,8 @@ interface IResourceAdapter {
|
|
|
137
147
|
/**
|
|
138
148
|
* @notice Raw receipt-token balance `holder` owns via `resource` (vToken / fToken / FRV
|
|
139
149
|
* shares), in receipt-token units — NOT underlying value.
|
|
140
|
-
* @dev
|
|
141
|
-
*
|
|
142
|
-
* would let removal orphan those receipt tokens; reading the raw balance closes that gap.
|
|
150
|
+
* @dev In share units so a sub-unit residual still reads as non-zero where a value read would
|
|
151
|
+
* round it to 0.
|
|
143
152
|
* @param resource Underlying yield protocol address.
|
|
144
153
|
* @param holder Address whose receipt balance to read (the YieldGroup).
|
|
145
154
|
* @return shares Receipt-token units `holder` holds in this resource.
|