@venusprotocol/liquidity-hub 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +422 -0
- package/contracts/Hub/Hub.sol +1614 -0
- package/contracts/Hub/HubStorage.sol +106 -0
- package/contracts/Hub/lib/HubAdminLib.sol +292 -0
- package/contracts/YieldGroup/YieldGroup.sol +128 -0
- package/contracts/YieldGroup/YieldGroupFRV.sol +107 -0
- package/contracts/YieldGroup/base/YieldGroupBase.sol +797 -0
- package/contracts/YieldGroup/base/YieldGroupBaseStorage.sol +73 -0
- package/contracts/adapters/AdapterCoreV1.sol +309 -0
- package/contracts/adapters/AdapterFRV.sol +276 -0
- package/contracts/adapters/AdapterFlux.sol +173 -0
- package/contracts/interfaces/IHub.sol +551 -0
- package/contracts/interfaces/IHubRegistry.sol +120 -0
- package/contracts/interfaces/IMigrator.sol +134 -0
- package/contracts/interfaces/IResourceAdapter.sol +154 -0
- package/contracts/interfaces/IYieldGroup.sol +119 -0
- package/contracts/interfaces/IYieldGroupBase.sol +507 -0
- package/contracts/interfaces/IYieldGroupFRV.sol +61 -0
- package/contracts/interfaces/external/IComptroller.sol +57 -0
- package/contracts/interfaces/external/IFRVVault.sol +113 -0
- package/contracts/interfaces/external/IFToken.sol +46 -0
- package/contracts/interfaces/external/IFluidLendingResolver.sol +63 -0
- package/contracts/interfaces/external/IVBep20.sol +39 -0
- package/contracts/interfaces/external/IVToken.sol +125 -0
- package/contracts/interfaces/external/IWBNB.sol +16 -0
- package/contracts/migrator/Migrator.sol +182 -0
- package/contracts/registry/HubRegistry.sol +179 -0
- package/deployments/bscmainnet/AdapterCoreV1.json +336 -0
- package/deployments/bscmainnet/AdapterFRV.json +225 -0
- package/deployments/bscmainnet/AdapterFlux.json +249 -0
- package/deployments/bscmainnet/CoreBeacon.json +110 -0
- package/deployments/bscmainnet/CoreSource_U.json +1083 -0
- package/deployments/bscmainnet/CoreSource_USDC.json +1083 -0
- package/deployments/bscmainnet/CoreSource_USDT.json +1083 -0
- package/deployments/bscmainnet/FRVBeacon.json +110 -0
- package/deployments/bscmainnet/FRVSource_U.json +978 -0
- package/deployments/bscmainnet/FRVSource_USDC.json +978 -0
- package/deployments/bscmainnet/FRVSource_USDT.json +978 -0
- package/deployments/bscmainnet/FluxBeacon.json +110 -0
- package/deployments/bscmainnet/FluxSource_U.json +1083 -0
- package/deployments/bscmainnet/FluxSource_USDC.json +1083 -0
- package/deployments/bscmainnet/FluxSource_USDT.json +1083 -0
- package/deployments/bscmainnet/HubBeacon.json +110 -0
- package/deployments/bscmainnet/HubImpl.json +2216 -0
- package/deployments/bscmainnet/HubRegistry.json +386 -0
- package/deployments/bscmainnet/HubRegistryImpl.json +386 -0
- package/deployments/bscmainnet/Hub_U.json +2216 -0
- package/deployments/bscmainnet/Hub_USDC.json +2216 -0
- package/deployments/bscmainnet/Hub_USDT.json +2216 -0
- package/deployments/bscmainnet/Migrator.json +375 -0
- package/deployments/bscmainnet/YieldGroupCoreImpl.json +1083 -0
- package/deployments/bscmainnet/YieldGroupFRVImpl.json +978 -0
- package/deployments/bscmainnet/YieldGroupFluxImpl.json +1083 -0
- package/deployments/bscmainnet_addresses.json +32 -0
- package/deployments/bsctestnet/AdapterCoreV1.json +336 -0
- package/deployments/bsctestnet/AdapterFRV.json +225 -0
- package/deployments/bsctestnet/AdapterFlux.json +249 -0
- package/deployments/bsctestnet/CoreBeacon.json +110 -0
- package/deployments/bsctestnet/CoreSource_USDT.json +1083 -0
- package/deployments/bsctestnet/FRVBeacon.json +110 -0
- package/deployments/bsctestnet/FRVSource_USDT.json +978 -0
- package/deployments/bsctestnet/FluxBeacon.json +110 -0
- package/deployments/bsctestnet/FluxSource_USDT.json +1083 -0
- package/deployments/bsctestnet/HubBeacon.json +110 -0
- package/deployments/bsctestnet/HubImpl.json +2216 -0
- package/deployments/bsctestnet/HubRegistry.json +386 -0
- package/deployments/bsctestnet/HubRegistryImpl.json +386 -0
- package/deployments/bsctestnet/HubRegistryProxyAdmin.json +157 -0
- package/deployments/bsctestnet/Hub_USDT.json +2216 -0
- package/deployments/bsctestnet/Migrator.json +375 -0
- package/deployments/bsctestnet/YieldGroupCoreImpl.json +1083 -0
- package/deployments/bsctestnet/YieldGroupFRVImpl.json +978 -0
- package/deployments/bsctestnet/YieldGroupFluxImpl.json +1083 -0
- package/deployments/bsctestnet_addresses.json +25 -0
- package/package.json +46 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { IYieldGroupBase } from "../../interfaces/IYieldGroupBase.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title YieldGroupBaseStorage
|
|
8
|
+
* @notice Shared state for every concrete YieldGroup (the generic `YieldGroup` serving Core / Flux
|
|
9
|
+
* and the `YieldGroupFRV` subclass), isolated in a base contract so upgrades can extend
|
|
10
|
+
* without disturbing the inheritance linearisation. Add new fields after `__gap` is shrunk
|
|
11
|
+
* by the same number of slots; never reorder, never prepend.
|
|
12
|
+
*
|
|
13
|
+
* @dev This is the UNION layout: every concrete shares this one slot table so a per-family beacon
|
|
14
|
+
* can be repointed at a different implementation without a storage migration. `_blocksPerYear`
|
|
15
|
+
* and `_resourceCap` are part of the union for layout stability even though a given family may
|
|
16
|
+
* not use them — the generic `YieldGroup` writes them; `YieldGroupFRV` leaves them at `0`.
|
|
17
|
+
*
|
|
18
|
+
* Slot layout (this contract's own contribution):
|
|
19
|
+
*
|
|
20
|
+
* Slot Used (bytes) Field
|
|
21
|
+
* ---- ------------ -----
|
|
22
|
+
* 0 20 _hub
|
|
23
|
+
* 1 20 _assetToken
|
|
24
|
+
* 2 32 _blocksPerYear (generic only; 0 for FRV / Flux deploys)
|
|
25
|
+
* 3 20 _accessControlManager
|
|
26
|
+
* 4 32 _resources
|
|
27
|
+
* 5 32 _resourceIndex
|
|
28
|
+
* 6 32 _resourcesConfig
|
|
29
|
+
* 7 32 _innerDepositQueue
|
|
30
|
+
* 8 32 _innerWithdrawQueue
|
|
31
|
+
* 9 32 _resourceCap (generic only; never written by FRV)
|
|
32
|
+
* 10..48 32 × 39 __gap
|
|
33
|
+
*/
|
|
34
|
+
abstract contract YieldGroupBaseStorage {
|
|
35
|
+
/// @notice Hub authorised to call `deposit` / `withdraw`. Set at initialisation.
|
|
36
|
+
address internal _hub;
|
|
37
|
+
|
|
38
|
+
/// @notice Underlying ERC-20 (matches `Hub.asset()`). Set at initialisation.
|
|
39
|
+
address internal _assetToken;
|
|
40
|
+
|
|
41
|
+
/// @notice Blocks-per-year constant used to annualise per-block supply rates. Set at
|
|
42
|
+
/// initialisation; per-chain (BNB Chain ≈ 10_512_000). `0` for families whose adapter
|
|
43
|
+
/// pre-annualises and ignores it (Flux), and for FRV.
|
|
44
|
+
uint256 internal _blocksPerYear;
|
|
45
|
+
|
|
46
|
+
/// @notice AccessControlManagerV8 gating governance functions. Set at initialisation.
|
|
47
|
+
address internal _accessControlManager;
|
|
48
|
+
|
|
49
|
+
/// @notice Canonical set of registered yield resources.
|
|
50
|
+
address[] internal _resources;
|
|
51
|
+
|
|
52
|
+
/// @notice 1-indexed position in `_resources` (0 = not registered). Enables O(1) removal.
|
|
53
|
+
mapping(address resource => uint256 indexPlusOne) internal _resourceIndex;
|
|
54
|
+
|
|
55
|
+
/// @notice Per-resource config (registration + pause + adapter).
|
|
56
|
+
mapping(address resource => IYieldGroupBase.ResourceConfig config) internal _resourcesConfig;
|
|
57
|
+
|
|
58
|
+
/// @notice Inner deposit cascade order. Set by Operator via ACM.
|
|
59
|
+
address[] internal _innerDepositQueue;
|
|
60
|
+
|
|
61
|
+
/// @notice Inner withdraw cascade order. Independent of the deposit queue.
|
|
62
|
+
address[] internal _innerWithdrawQueue;
|
|
63
|
+
|
|
64
|
+
/// @notice Optional per-resource deposit cap, in asset units. `0` = unset/unbounded
|
|
65
|
+
/// (only the resource's protocol-level supply cap binds). When non-zero, the
|
|
66
|
+
/// effective deposit room is `min(protocol headroom, cap − our balance)`. Only the
|
|
67
|
+
/// generic `YieldGroup` reads / writes this; FRV leaves it `0`.
|
|
68
|
+
mapping(address resource => uint256 cap) internal _resourceCap;
|
|
69
|
+
|
|
70
|
+
/// @dev Reserved slots for future state. Shrink by the number of slots a new field
|
|
71
|
+
/// consumes when extending.
|
|
72
|
+
uint256[39] private __gap;
|
|
73
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
6
|
+
|
|
7
|
+
import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
|
|
8
|
+
import { IVToken } from "../interfaces/external/IVToken.sol";
|
|
9
|
+
import { IComptroller } from "../interfaces/external/IComptroller.sol";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @title AdapterCoreV1
|
|
13
|
+
* @author Venus
|
|
14
|
+
* @notice Stateless adapter for Venus Core V1 vTokens. ONE deployment serves every
|
|
15
|
+
* YieldGroup that registers a Core V1 market — there is no per-(YieldGroup, vToken)
|
|
16
|
+
* instance, no proxy, no clone factory.
|
|
17
|
+
* @dev Dispatch model: mutating functions (`deposit`, `withdraw`) MUST be invoked via
|
|
18
|
+
* DELEGATECALL from a YieldGroup. They execute in the YieldGroup's storage context,
|
|
19
|
+
* so receipt-token credits and debits land on the YieldGroup, not this adapter.
|
|
20
|
+
* View functions are invoked via normal CALL / STATICCALL with an explicit `holder`
|
|
21
|
+
* argument.
|
|
22
|
+
*
|
|
23
|
+
* Storage-safety invariants enforced by this contract:
|
|
24
|
+
* 1. Zero `internal`/`private`/`public` state variables are declared. Only `immutable`
|
|
25
|
+
* values are used.
|
|
26
|
+
* 2. No inline assembly performs `sstore`.
|
|
27
|
+
* 3. All external calls go to the supplied `resource` (vToken), its `underlying()`, or
|
|
28
|
+
* its `comptroller()`. No arbitrary user-supplied call target.
|
|
29
|
+
* 4. {onlyDelegateCall} reverts when a mutating function is invoked directly on the
|
|
30
|
+
* adapter, preventing accidents that would orphan vTokens here.
|
|
31
|
+
*
|
|
32
|
+
* Treasury-fee handling: when the Comptroller's `treasuryPercent != 0`,
|
|
33
|
+
* `redeemUnderlying(X)` delivers only `X − fee` to the caller. `withdraw` grosses up
|
|
34
|
+
* the request via ceil-division so the YieldGroup receives at least `amount`; any
|
|
35
|
+
* surplus remains as idle on the YieldGroup (counted in `totalAssets`).
|
|
36
|
+
*
|
|
37
|
+
* vBNB is unsupported by design: `IVToken(resource).underlying()` reverts on vBNB,
|
|
38
|
+
* which trips `asset()` and prevents the YieldGroup from registering it.
|
|
39
|
+
*/
|
|
40
|
+
contract AdapterCoreV1 is IResourceAdapter {
|
|
41
|
+
using SafeERC20 for IERC20;
|
|
42
|
+
|
|
43
|
+
// ============================== State (immutable only) ====================
|
|
44
|
+
|
|
45
|
+
/// @notice This adapter's own address, captured at deployment. Used by
|
|
46
|
+
/// {onlyDelegateCall} to detect direct invocation. Immutables live in bytecode,
|
|
47
|
+
/// not storage, so they're delegatecall-safe.
|
|
48
|
+
address private immutable _ADAPTER_SELF;
|
|
49
|
+
|
|
50
|
+
// ============================== State (constants) =========================
|
|
51
|
+
|
|
52
|
+
/// @notice Mantissa one used in treasury-fee math (`1e18`).
|
|
53
|
+
uint256 public constant MANTISSA_ONE = 1e18;
|
|
54
|
+
|
|
55
|
+
/// @notice Fixed-point scale matching Compound's mantissa (`1e18`).
|
|
56
|
+
uint256 public constant EXP_SCALE = 1e18;
|
|
57
|
+
|
|
58
|
+
/// @notice Scaling factor between mantissa-rate (`1e18`) and BPS (`1e4`).
|
|
59
|
+
uint256 internal constant MANTISSA_TO_BPS = 1e14;
|
|
60
|
+
|
|
61
|
+
// ============================== Errors ===================================
|
|
62
|
+
|
|
63
|
+
/// @notice A mutating function was invoked directly (not via delegatecall).
|
|
64
|
+
error NotDelegateCall();
|
|
65
|
+
|
|
66
|
+
/// @notice A vToken `mint` call returned a non-zero error code.
|
|
67
|
+
/// @param resource vToken that rejected the mint.
|
|
68
|
+
/// @param errorCode Compound-style error code returned by the vToken.
|
|
69
|
+
error VTokenMintFailed(address resource, uint256 errorCode);
|
|
70
|
+
|
|
71
|
+
/// @notice A vToken `redeemUnderlying` call returned a non-zero error code.
|
|
72
|
+
/// @param resource vToken that rejected the redeem.
|
|
73
|
+
/// @param errorCode Compound-style error code returned by the vToken.
|
|
74
|
+
error VTokenRedeemFailed(address resource, uint256 errorCode);
|
|
75
|
+
|
|
76
|
+
/// @notice A vToken `accrueInterest` call returned a non-zero error code.
|
|
77
|
+
/// @param resource vToken that failed to accrue.
|
|
78
|
+
/// @param errorCode Compound-style error code returned by the vToken.
|
|
79
|
+
error VTokenAccrueFailed(address resource, uint256 errorCode);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @notice A redeem succeeded but delivered less than the requested net amount. Indicates
|
|
83
|
+
* a fee-on-transfer underlying, an unexpected treasury cut, or a vToken bug.
|
|
84
|
+
* @param resource Resource that under-delivered.
|
|
85
|
+
* @param requested Net underlying units expected.
|
|
86
|
+
* @param actual Underlying units actually received.
|
|
87
|
+
*/
|
|
88
|
+
error VTokenUnderfilled(address resource, uint256 requested, uint256 actual);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @notice A vToken's Comptroller charges a non-zero `treasuryPercent`. That exit fee leaves
|
|
92
|
+
* the pool on every redeem without burning shares, so it is unmodeled in the Hub's
|
|
93
|
+
* gross NAV and would overstate PPS — rejected at registration.
|
|
94
|
+
* @param resource vToken whose Comptroller charges the fee.
|
|
95
|
+
* @param treasuryPercent Offending treasury percent (mantissa).
|
|
96
|
+
*/
|
|
97
|
+
error TreasuryFeeUnsupported(address resource, uint256 treasuryPercent);
|
|
98
|
+
|
|
99
|
+
// ============================== Modifiers ================================
|
|
100
|
+
|
|
101
|
+
/// @notice Reverts {NotDelegateCall} when the call is not arriving via delegatecall.
|
|
102
|
+
/// @dev During a delegatecall, `address(this)` is the caller's address; during a direct
|
|
103
|
+
/// call, it equals `_ADAPTER_SELF` (the address captured at construction).
|
|
104
|
+
modifier onlyDelegateCall() {
|
|
105
|
+
if (address(this) == _ADAPTER_SELF) revert NotDelegateCall();
|
|
106
|
+
_;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================== Constructor ==============================
|
|
110
|
+
|
|
111
|
+
/// @notice Captures the adapter's own address into an immutable for {onlyDelegateCall}.
|
|
112
|
+
constructor() {
|
|
113
|
+
_ADAPTER_SELF = address(this);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================== External — mutating =======================
|
|
117
|
+
|
|
118
|
+
/// @inheritdoc IResourceAdapter
|
|
119
|
+
function deposit(address resource, uint256 amount) external override onlyDelegateCall returns (uint256 deposited) {
|
|
120
|
+
// `address(this)` here is the YieldGroup (delegatecall context). The YieldGroup
|
|
121
|
+
// already holds `amount` of `underlying`; we just approve the vToken and mint —
|
|
122
|
+
// vTokens are credited to `msg.sender` which, from the vToken's perspective, is
|
|
123
|
+
// the YieldGroup (delegatecall preserves caller context to the next call).
|
|
124
|
+
IERC20 assetToken = IERC20(IVToken(resource).underlying());
|
|
125
|
+
SafeERC20.forceApprove(assetToken, resource, amount);
|
|
126
|
+
uint256 errCode = IVToken(resource).mint(amount);
|
|
127
|
+
if (errCode != 0) revert VTokenMintFailed(resource, errCode);
|
|
128
|
+
return amount;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// @inheritdoc IResourceAdapter
|
|
132
|
+
function withdraw(address resource, uint256 amount, address to) external override onlyDelegateCall {
|
|
133
|
+
// `address(this)` here is the YieldGroup. `vToken.redeemUnderlying` burns its
|
|
134
|
+
// (YieldGroup's) vTokens and credits the underlying back to the YieldGroup.
|
|
135
|
+
IComptroller comptrollerContract = IComptroller(IVToken(resource).comptroller());
|
|
136
|
+
IERC20 assetToken = IERC20(IVToken(resource).underlying());
|
|
137
|
+
|
|
138
|
+
uint256 grossRedeem = _floorRedeemToOneVToken(resource, _grossUpForTreasury(comptrollerContract, amount));
|
|
139
|
+
uint256 preBal = assetToken.balanceOf(address(this));
|
|
140
|
+
uint256 errCode = IVToken(resource).redeemUnderlying(grossRedeem);
|
|
141
|
+
if (errCode != 0) revert VTokenRedeemFailed(resource, errCode);
|
|
142
|
+
uint256 received = assetToken.balanceOf(address(this)) - preBal;
|
|
143
|
+
if (received < amount) revert VTokenUnderfilled(resource, amount, received);
|
|
144
|
+
|
|
145
|
+
// Transfer exactly `amount` to `to`. Any gross-up surplus stays on the YieldGroup
|
|
146
|
+
// as idle balance — it's counted in `YieldGroup.totalAssets` and consumed on the
|
|
147
|
+
// next withdrawal's idle-first path, so dust is never socialised across YieldGroups.
|
|
148
|
+
assetToken.safeTransfer(to, amount);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// @inheritdoc IResourceAdapter
|
|
152
|
+
/// @dev Plain CALL, no `onlyDelegateCall`: `accrueInterest` mutates the vToken's own global
|
|
153
|
+
/// index, not this caller's position, so it is safe (and cheaper) to run in the adapter's
|
|
154
|
+
/// context. After it returns, `exchangeRateStored()` is fresh to the current block.
|
|
155
|
+
function accrue(address resource) external override {
|
|
156
|
+
uint256 errCode = IVToken(resource).accrueInterest();
|
|
157
|
+
if (errCode != 0) revert VTokenAccrueFailed(resource, errCode);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================== External — view ==========================
|
|
161
|
+
|
|
162
|
+
/// @inheritdoc IResourceAdapter
|
|
163
|
+
function asset(address resource) external view override returns (address) {
|
|
164
|
+
return IVToken(resource).underlying();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// @inheritdoc IResourceAdapter
|
|
168
|
+
/// @dev Values the position at REALIZABLE (net-of-treasury) terms so NAV/PPS equal the value the
|
|
169
|
+
/// position can actually deliver. `withdraw` already nets the Venus treasury exit cut, so
|
|
170
|
+
/// valuing gross here would overstate NAV and let a first-out LP exit at the pre-fee price
|
|
171
|
+
/// while the shortfall socialises onto the last-out LP. When the Comptroller fee is off
|
|
172
|
+
/// (every registered market today) this returns the gross value unchanged.
|
|
173
|
+
function totalAssets(address resource, address holder) external view override returns (uint256) {
|
|
174
|
+
uint256 vBal = IVToken(resource).balanceOf(holder);
|
|
175
|
+
if (vBal == 0) return 0;
|
|
176
|
+
uint256 gross = (vBal * IVToken(resource).exchangeRateStored()) / EXP_SCALE;
|
|
177
|
+
|
|
178
|
+
uint256 treasuryPct = IComptroller(IVToken(resource).comptroller()).treasuryPercent();
|
|
179
|
+
if (treasuryPct == 0) return gross;
|
|
180
|
+
// The Comptroller setter enforces `treasuryPercent < 1e18`, so the subtraction cannot underflow.
|
|
181
|
+
return (gross * (MANTISSA_ONE - treasuryPct)) / MANTISSA_ONE;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// @inheritdoc IResourceAdapter
|
|
185
|
+
/// @dev Honors Comptroller mint pause and supply cap. `supplyCap == 0` rejects mint
|
|
186
|
+
/// outright per PolicyFacet, so we surface that as 0 capacity. The headroom math
|
|
187
|
+
/// mirrors PolicyFacet's `nextTotalSupply = totalSupply × ER + mintAmount` check.
|
|
188
|
+
function maxDeposit(address resource) external view override returns (uint256) {
|
|
189
|
+
IComptroller comptrollerContract = IComptroller(IVToken(resource).comptroller());
|
|
190
|
+
// Action.MINT == 0 in the Venus Comptroller's enum.
|
|
191
|
+
if (comptrollerContract.actionPaused(resource, uint8(0))) return 0;
|
|
192
|
+
uint256 supplyCap = comptrollerContract.supplyCaps(resource);
|
|
193
|
+
if (supplyCap == 0) return 0;
|
|
194
|
+
|
|
195
|
+
uint256 totalSupplied = (IVToken(resource).totalSupply() * IVToken(resource).exchangeRateStored()) / EXP_SCALE;
|
|
196
|
+
uint256 raw = supplyCap > totalSupplied ? supplyCap - totalSupplied : 0;
|
|
197
|
+
// `exchangeRateStored` is pre-accrual, but Venus re-checks the supply cap at mint AFTER
|
|
198
|
+
// `accrueInterest` raises the rate, so the raw headroom is slightly optimistic on an
|
|
199
|
+
// interest-accruing near-cap market. Trim a small conservative margin so a normal accrual
|
|
200
|
+
// between this view and execution cannot push `deposit(maxDeposit())` over the cap. The
|
|
201
|
+
// routing cascade is the backstop for larger staleness (an over-cap leg is skipped, not
|
|
202
|
+
// bubbled), so this only needs to absorb a typical inter-block accrual.
|
|
203
|
+
return raw - (raw / 1000);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// @inheritdoc IResourceAdapter
|
|
207
|
+
/// @dev Three constraints: (a) what `holder` holds in the resource (vToken balance × ER),
|
|
208
|
+
/// (b) what the vToken can pay out in cash (`getCash`), and (c) the treasury cut
|
|
209
|
+
/// (`treasuryPercent`). The min of (a) and (b) is the gross redeemable; multiplying
|
|
210
|
+
/// by `(1 − treasuryPct)` reflects what actually arrives at the recipient.
|
|
211
|
+
function maxWithdraw(address resource, address holder) external view override returns (uint256) {
|
|
212
|
+
IComptroller comptrollerContract = IComptroller(IVToken(resource).comptroller());
|
|
213
|
+
// Action.REDEEM == 1 in the Venus Comptroller's enum.
|
|
214
|
+
if (comptrollerContract.actionPaused(resource, uint8(1))) return 0;
|
|
215
|
+
|
|
216
|
+
uint256 vBal = IVToken(resource).balanceOf(holder);
|
|
217
|
+
if (vBal == 0) return 0;
|
|
218
|
+
uint256 exchangeRate = IVToken(resource).exchangeRateStored();
|
|
219
|
+
uint256 ourValue = (vBal * exchangeRate) / EXP_SCALE;
|
|
220
|
+
if (ourValue == 0) return 0;
|
|
221
|
+
|
|
222
|
+
uint256 cash = IVToken(resource).getCash();
|
|
223
|
+
// A redeem that would burn 0 vTokens is bumped to one whole vToken unit of underlying by
|
|
224
|
+
// `_floorRedeemToOneVToken`. If the market's cash is below that unit, every non-zero redeem
|
|
225
|
+
// the cascade could try exceeds cash and reverts in Venus `redeemFresh`. Report 0 liquidity
|
|
226
|
+
// rather than certify a withdraw the market cannot settle.
|
|
227
|
+
uint256 oneVTokenUnit = (exchangeRate + EXP_SCALE - 1) / EXP_SCALE;
|
|
228
|
+
if (cash < oneVTokenUnit) return 0;
|
|
229
|
+
|
|
230
|
+
uint256 redeemableGross = ourValue < cash ? ourValue : cash;
|
|
231
|
+
if (redeemableGross == 0) return 0;
|
|
232
|
+
|
|
233
|
+
uint256 treasuryPct = comptrollerContract.treasuryPercent();
|
|
234
|
+
if (treasuryPct == 0) return redeemableGross;
|
|
235
|
+
// The Comptroller setter enforces `treasuryPercent < 1e18`, so the subtraction cannot underflow.
|
|
236
|
+
return (redeemableGross * (MANTISSA_ONE - treasuryPct)) / MANTISSA_ONE;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// @inheritdoc IResourceAdapter
|
|
240
|
+
function spotAPYBps(address resource, uint256 blocksPerYear) external view override returns (uint64) {
|
|
241
|
+
uint256 annualised = (IVToken(resource).supplyRatePerBlock() * blocksPerYear) / MANTISSA_TO_BPS;
|
|
242
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
243
|
+
return annualised > type(uint64).max ? type(uint64).max : uint64(annualised);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// @inheritdoc IResourceAdapter
|
|
247
|
+
function receiptBalance(address resource, address holder) external view override returns (uint256) {
|
|
248
|
+
return IVToken(resource).balanceOf(holder);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// @inheritdoc IResourceAdapter
|
|
252
|
+
/// @dev Rejects a market whose Comptroller charges a non-zero `treasuryPercent`: that exit fee
|
|
253
|
+
/// is unmodeled in the Hub's gross NAV (see the treasury-fee note above). The withdraw
|
|
254
|
+
/// gross-up still keeps individual redeems whole if Venus governance turns the fee on
|
|
255
|
+
/// *after* registration; this guard only blocks registering a market that already has it.
|
|
256
|
+
function validateRegistration(address resource) external view override {
|
|
257
|
+
uint256 treasuryPct = IComptroller(IVToken(resource).comptroller()).treasuryPercent();
|
|
258
|
+
if (treasuryPct != 0) revert TreasuryFeeUnsupported(resource, treasuryPct);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================== Private — view ===========================
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @notice Gross-up a desired net delivery amount for the Comptroller's treasury cut.
|
|
265
|
+
* @dev Ceil-division ensures `received >= netAmount`; any rounding surplus stays as
|
|
266
|
+
* idle on the YieldGroup, never on this adapter.
|
|
267
|
+
* @param comptrollerContract Comptroller to read `treasuryPercent` from.
|
|
268
|
+
* @param netAmount Underlying units the caller wants delivered.
|
|
269
|
+
* @return grossAmount Underlying units to request from `redeemUnderlying`.
|
|
270
|
+
*/
|
|
271
|
+
function _grossUpForTreasury(IComptroller comptrollerContract, uint256 netAmount)
|
|
272
|
+
private
|
|
273
|
+
view
|
|
274
|
+
returns (uint256 grossAmount)
|
|
275
|
+
{
|
|
276
|
+
uint256 treasuryPct = comptrollerContract.treasuryPercent();
|
|
277
|
+
if (treasuryPct == 0) return netAmount;
|
|
278
|
+
// The Comptroller setter enforces `treasuryPercent < 1e18`, so the denominator is always non-zero.
|
|
279
|
+
uint256 denominator = MANTISSA_ONE - treasuryPct;
|
|
280
|
+
return (netAmount * MANTISSA_ONE + denominator - 1) / denominator;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @notice Raise a sub-one-vToken redeem up to exactly one vToken unit of underlying.
|
|
285
|
+
* @dev Compound computes `redeemTokens = floor(redeemAmount * 1e18 / exchangeRate)` and reverts
|
|
286
|
+
* `"redeemTokens zero"` when that floors to 0. A withdraw cascade can legitimately hand this
|
|
287
|
+
* adapter a dust `amount` — e.g. a 1-wei remainder left after an upstream ERC-4626 yieldGroup
|
|
288
|
+
* (Flux/FRV) rounds its own redeem down — which would otherwise revert an entirely valid,
|
|
289
|
+
* within-`maxWithdraw` withdrawal. When the requested redeem would burn 0 tokens, redeem one
|
|
290
|
+
* whole vToken unit instead; `withdraw` then transfers exactly `amount` and leaves the
|
|
291
|
+
* surplus idle on the YieldGroup (counted in `totalAssets`, consumed idle-first on the next
|
|
292
|
+
* withdrawal) — identical to how the treasury gross-up surplus is handled. A no-op for
|
|
293
|
+
* normal-sized redeems and for unit exchange rates (`amount >= 1` never floors to 0 there).
|
|
294
|
+
* @param resource vToken being redeemed from.
|
|
295
|
+
* @param grossRedeem Underlying units the caller intends to redeem.
|
|
296
|
+
* @return bumped `grossRedeem`, raised to `ceil(exchangeRate / 1e18)` only if it would burn 0 tokens.
|
|
297
|
+
*/
|
|
298
|
+
function _floorRedeemToOneVToken(address resource, uint256 grossRedeem) private view returns (uint256 bumped) {
|
|
299
|
+
uint256 exchangeRate = IVToken(resource).exchangeRateStored();
|
|
300
|
+
// Venus burns `redeemTokens = floor(grossRedeem * 1e18 / exchangeRate)` and reverts when that
|
|
301
|
+
// is 0 — i.e. when `grossRedeem * 1e18 < exchangeRate`. In that case redeem exactly one vToken
|
|
302
|
+
// unit of underlying (`ceil(exchangeRate / 1e18)`) so the burn is non-zero; the caller still
|
|
303
|
+
// transfers only `amount` and leaves the surplus idle on the YieldGroup.
|
|
304
|
+
if (exchangeRate != 0 && grossRedeem * EXP_SCALE < exchangeRate) {
|
|
305
|
+
return (exchangeRate + EXP_SCALE - 1) / EXP_SCALE;
|
|
306
|
+
}
|
|
307
|
+
return grossRedeem;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
6
|
+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
|
|
7
|
+
|
|
8
|
+
import { IResourceAdapter } from "../interfaces/IResourceAdapter.sol";
|
|
9
|
+
import { IFRVVault } from "../interfaces/external/IFRVVault.sol";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @title AdapterFRV
|
|
13
|
+
* @author Venus
|
|
14
|
+
* @notice Stateless adapter for Venus Fixed Rate Vaults (ERC-4626). ONE deployment serves
|
|
15
|
+
* every YieldGroupFRV that registers an FRV vault — there is no per-(YieldGroup, vault)
|
|
16
|
+
* instance, no proxy, no clone factory.
|
|
17
|
+
* @dev Dispatch model: mutating functions (`deposit`, `withdraw`) MUST be invoked via
|
|
18
|
+
* DELEGATECALL from a YieldGroupFRV. They execute in the YieldGroup's storage context,
|
|
19
|
+
* so ERC-4626 share credits and debits land on the YieldGroup, not this adapter.
|
|
20
|
+
* View functions are invoked via normal CALL / STATICCALL with an explicit `holder`
|
|
21
|
+
* argument.
|
|
22
|
+
*
|
|
23
|
+
* Storage-safety invariants enforced by this contract:
|
|
24
|
+
* 1. Zero `internal`/`private`/`public` state variables are declared. Only `immutable`
|
|
25
|
+
* values (and `constant`s, which live in bytecode) are used.
|
|
26
|
+
* 2. No inline assembly performs `sstore`.
|
|
27
|
+
* 3. All external calls go to the supplied `resource` (FRV vault) or its `asset()`.
|
|
28
|
+
* 4. {onlyDelegateCall} reverts when a mutating function is invoked directly on the
|
|
29
|
+
* adapter, preventing accidents that would orphan shares here.
|
|
30
|
+
*
|
|
31
|
+
* No treasury-fee handling: FRV takes its `reserveFactor` cut once at settlement
|
|
32
|
+
* (`_settleProtocolShare`), not per-redeem. No gross-up needed on withdraw.
|
|
33
|
+
*
|
|
34
|
+
* Valuation model (time-based linear accrual): a live FRV position in Lock is valued at its
|
|
35
|
+
* principal plus the coupon accrued so far on a straight line over the lock — `principal +
|
|
36
|
+
* netInterest * elapsed / lockDuration`, where `netInterest` is the full-term coupon net of the
|
|
37
|
+
* vault's `reserveFactor`. The arithmetic mirrors `BaseVault` exactly so that at `elapsed ==
|
|
38
|
+
* lockDuration` the value equals the vault's full-repayment `settlementAmount`. The Lock ->
|
|
39
|
+
* Matured transition is therefore continuous ONLY on full repayment; on a partial / principal
|
|
40
|
+
* shortfall the realized `settlementAmount` is lower, so the mark steps DOWN at settlement (and
|
|
41
|
+
* a defaulting position in PendingSettlement / SettlementDeadlineExceeded is marked above its
|
|
42
|
+
* realizable value). That over-mark is by design a forward-looking accrual; a principal shortfall
|
|
43
|
+
* on a defaulting loan is bounded by governance's per-source cap (the Hub may additionally apply
|
|
44
|
+
* an exit fee as anti-sandwich friction). `totalAssets` returns this accrued value directly as
|
|
45
|
+
* the position's Hub-facing NAV.
|
|
46
|
+
*
|
|
47
|
+
* Cap interaction: this mark rises over the lock while the coupon stays non-withdrawable
|
|
48
|
+
* (`maxWithdraw` is 0 until the vault is terminal), so it eats into the YieldGroup's Hub-side cap
|
|
49
|
+
* headroom over time — a group can reach or exceed its cap on accrual alone and block further
|
|
50
|
+
* deposits / reallocation into it. Fail-closed: it only tightens capacity. See `Hub._effectiveCap`.
|
|
51
|
+
*
|
|
52
|
+
* Basis invariant: the principal / gross slices use the vault's `+1 / +1` floored ERC-4626
|
|
53
|
+
* conversion (`mulDiv(shares, assets + 1, supply + 1, Down)`), which is exact ONLY while the FRV
|
|
54
|
+
* vault leaves `_decimalsOffset()` at 0 (it does, and defines no override). If a future FRV
|
|
55
|
+
* family overrides it, this adapter must be updated in lockstep.
|
|
56
|
+
*
|
|
57
|
+
* FRV state machine (`updateVaultState()`) is NOT called here — `YieldGroupFRV` calls
|
|
58
|
+
* it before invoking the adapter so that state-dependent reads reflect the latest state.
|
|
59
|
+
* The valuation is time-based, so it needs no state poke of its own; `accrue` is a no-op.
|
|
60
|
+
*/
|
|
61
|
+
contract AdapterFRV is IResourceAdapter {
|
|
62
|
+
using SafeERC20 for IERC20;
|
|
63
|
+
|
|
64
|
+
// ============================== State (immutable only) ====================
|
|
65
|
+
|
|
66
|
+
address private immutable _ADAPTER_SELF;
|
|
67
|
+
|
|
68
|
+
// ============================== Constants =================================
|
|
69
|
+
// Mirror `BaseVault` (BPS = 10_000, MANTISSA_ONE = 1e18, YEAR = 365 days) so the accrued value
|
|
70
|
+
// is computed on the identical basis as the vault's own interest and settlement math.
|
|
71
|
+
|
|
72
|
+
uint256 private constant BPS = 10_000;
|
|
73
|
+
uint256 private constant MANTISSA_ONE = 1e18;
|
|
74
|
+
uint256 private constant YEAR = 365 days;
|
|
75
|
+
|
|
76
|
+
// ============================== Errors ===================================
|
|
77
|
+
|
|
78
|
+
error NotDelegateCall();
|
|
79
|
+
|
|
80
|
+
// ============================== Modifiers ================================
|
|
81
|
+
|
|
82
|
+
modifier onlyDelegateCall() {
|
|
83
|
+
if (address(this) == _ADAPTER_SELF) revert NotDelegateCall();
|
|
84
|
+
_;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================== Constructor ==============================
|
|
88
|
+
|
|
89
|
+
constructor() {
|
|
90
|
+
_ADAPTER_SELF = address(this);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================== External — mutating =======================
|
|
94
|
+
|
|
95
|
+
/// @inheritdoc IResourceAdapter
|
|
96
|
+
function deposit(address resource, uint256 amount) external override onlyDelegateCall returns (uint256 deposited) {
|
|
97
|
+
IERC20 assetToken = IERC20(IFRVVault(resource).asset());
|
|
98
|
+
assetToken.forceApprove(resource, amount);
|
|
99
|
+
IFRVVault(resource).deposit(amount, address(this));
|
|
100
|
+
return amount;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// @inheritdoc IResourceAdapter
|
|
104
|
+
function withdraw(address resource, uint256 amount, address to) external override onlyDelegateCall {
|
|
105
|
+
IFRVVault(resource).withdraw(amount, to, address(this));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// @inheritdoc IResourceAdapter
|
|
109
|
+
/// @dev No-op: the FRV valuation is time-based and computed on-demand in {totalAssets} from the
|
|
110
|
+
/// vault's lock window, so there is no block-lazy exchange-rate state to freshen here.
|
|
111
|
+
// solhint-disable-next-line no-empty-blocks
|
|
112
|
+
function accrue(address resource) external override { }
|
|
113
|
+
|
|
114
|
+
// ============================== External — view ==========================
|
|
115
|
+
|
|
116
|
+
/// @inheritdoc IResourceAdapter
|
|
117
|
+
function asset(address resource) external view override returns (address) {
|
|
118
|
+
return IFRVVault(resource).asset();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// @inheritdoc IResourceAdapter
|
|
122
|
+
/// @dev Position value = principal slice + accrued-coupon slice; this is the Hub-facing NAV for
|
|
123
|
+
/// the position. See the contract NatSpec for the valuation model.
|
|
124
|
+
function totalAssets(address resource, address holder) external view override returns (uint256) {
|
|
125
|
+
(uint256 principalValue, uint256 couponValueGross) = _breakdown(resource, holder);
|
|
126
|
+
return principalValue + couponValueGross;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// @inheritdoc IResourceAdapter
|
|
130
|
+
/// @dev FRV's `maxDeposit()` returns 0 outside Fundraising state. The receiver param
|
|
131
|
+
/// is ignored by the FRV vault (capacity is global, not per-address).
|
|
132
|
+
/// Pause-blind, like the vault's own `maxDeposit`; a paused vault is caught and skipped by
|
|
133
|
+
/// the YieldGroup deposit cascade, not gated here.
|
|
134
|
+
function maxDeposit(address resource) external view override returns (uint256) {
|
|
135
|
+
return IFRVVault(resource).maxDeposit(address(this));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// @inheritdoc IResourceAdapter
|
|
139
|
+
/// @dev FRV's `maxWithdraw()` returns 0 outside terminal states (Matured/Failed/Liquidated).
|
|
140
|
+
/// No treasury-fee deduction needed — FRV takes its cut at settlement, not per-redeem.
|
|
141
|
+
/// The accrued coupon surfaced by {totalAssets} is NOT withdrawable mid-lock; this returning
|
|
142
|
+
/// 0 during Lock is what keeps the rising NAV non-deliverable until settlement.
|
|
143
|
+
/// Pause-blind, like the vault's own `maxWithdraw` (it ignores `pauseLevel`): a completely
|
|
144
|
+
/// paused vault is caught and skipped by the YieldGroup withdraw cascade's `try/catch`, not
|
|
145
|
+
/// gated here.
|
|
146
|
+
function maxWithdraw(address resource, address holder) external view override returns (uint256) {
|
|
147
|
+
return IFRVVault(resource).maxWithdraw(holder);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// @inheritdoc IResourceAdapter
|
|
151
|
+
/// @dev Returns the vault's `fixedAPY` (already annualised in BPS) only while the vault is
|
|
152
|
+
/// actually earning — `Fundraising` or `Lock`. In any other state the capital earns
|
|
153
|
+
/// nothing (matured/idle awaiting redemption, failed, liquidated, or pre-fundraising),
|
|
154
|
+
/// so report 0 rather than the contractual rate; otherwise dead capital would inflate
|
|
155
|
+
/// the YieldGroup's TVL-weighted spot APY. The `blocksPerYear` parameter is unused since FRV
|
|
156
|
+
/// yields are time-based, not block-based.
|
|
157
|
+
function spotAPYBps(
|
|
158
|
+
address resource,
|
|
159
|
+
uint256 /* blocksPerYear */
|
|
160
|
+
)
|
|
161
|
+
external
|
|
162
|
+
view
|
|
163
|
+
override
|
|
164
|
+
returns (uint64)
|
|
165
|
+
{
|
|
166
|
+
IFRVVault.FRVVaultState state = IFRVVault(resource).state();
|
|
167
|
+
if (state != IFRVVault.FRVVaultState.Fundraising && state != IFRVVault.FRVVaultState.Lock) {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
IFRVVault.FRVVaultConfig memory cfg = IFRVVault(resource).config();
|
|
171
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
172
|
+
return cfg.fixedAPY > type(uint64).max ? type(uint64).max : uint64(cfg.fixedAPY);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// @inheritdoc IResourceAdapter
|
|
176
|
+
function receiptBalance(address resource, address holder) external view override returns (uint256) {
|
|
177
|
+
return IFRVVault(resource).balanceOf(holder);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// @inheritdoc IResourceAdapter
|
|
181
|
+
/// @dev No-op: FRV takes its `reserveFactor` cut once at settlement, not per-redeem, so there
|
|
182
|
+
/// is no unmodeled exit fee to guard against at registration.
|
|
183
|
+
function validateRegistration(address resource) external pure override {
|
|
184
|
+
// No FRV-specific registration precondition. `resource` validity is established by the
|
|
185
|
+
// YieldGroup's `asset()` probe and `code.length` check at registration time.
|
|
186
|
+
resource;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ============================== Internal — view ==========================
|
|
190
|
+
|
|
191
|
+
/// @dev Split the holder's position into (principalValue, couponValueGross), both in underlying.
|
|
192
|
+
/// Branches by lifecycle state:
|
|
193
|
+
/// - no shares -> (0, 0)
|
|
194
|
+
/// - Closed -> (0, 0) (stuck residual; valued at 0 = deliverable value)
|
|
195
|
+
/// - terminal (Matured / Failed / Liquidated) -> (previewRedeem(shares), 0) (realized)
|
|
196
|
+
/// - otherwise -> principal slice + accrued-coupon slice
|
|
197
|
+
/// Both slices use the vault's own +1/+1 floored ERC-4626 basis (decimalsOffset 0), so the
|
|
198
|
+
/// principal slice equals `previewRedeem(shares)` during non-terminal states and the gross
|
|
199
|
+
/// sum equals the post-Matured `previewRedeem` at `elapsed == lockDuration`.
|
|
200
|
+
function _breakdown(address resource, address holder)
|
|
201
|
+
internal
|
|
202
|
+
view
|
|
203
|
+
returns (uint256 principalValue, uint256 couponValueGross)
|
|
204
|
+
{
|
|
205
|
+
IFRVVault vault = IFRVVault(resource);
|
|
206
|
+
uint256 shares = vault.balanceOf(holder);
|
|
207
|
+
if (shares == 0) return (0, 0);
|
|
208
|
+
|
|
209
|
+
IFRVVault.FRVVaultRuntime memory rt = vault.runtime();
|
|
210
|
+
IFRVVault.FRVVaultState state = rt.state;
|
|
211
|
+
|
|
212
|
+
// Closed is not loss-adjusted by the vault (its `totalAssets()` resurrects `totalRaised`),
|
|
213
|
+
// yet the position is non-withdrawable — value it at 0 so reported NAV == deliverable value.
|
|
214
|
+
if (state == IFRVVault.FRVVaultState.Closed) return (0, 0);
|
|
215
|
+
|
|
216
|
+
// Terminal states have settled to a realized `settlementAmount`; `previewRedeem` is the
|
|
217
|
+
// deliverable value (can be below principal on a loss). No unrealized coupon remains.
|
|
218
|
+
if (
|
|
219
|
+
state == IFRVVault.FRVVaultState.Matured || state == IFRVVault.FRVVaultState.Failed
|
|
220
|
+
|| state == IFRVVault.FRVVaultState.Liquidated
|
|
221
|
+
) {
|
|
222
|
+
return (vault.previewRedeem(shares), 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
uint256 supply = vault.totalSupply();
|
|
226
|
+
if (supply == 0) return (0, 0); // defensive; a holder with shares implies supply > 0
|
|
227
|
+
|
|
228
|
+
// Principal slice on the vault's own +1/+1 floored basis. During Lock the vault reports
|
|
229
|
+
// `totalAssets() == totalRaised`, so this equals `previewRedeem(shares)` exactly.
|
|
230
|
+
principalValue = Math.mulDiv(shares, rt.totalRaised + 1, supply + 1, Math.Rounding.Down);
|
|
231
|
+
|
|
232
|
+
uint256 accruedTotalAssets = _accruedTotalAssets(vault, rt);
|
|
233
|
+
// accruedTotalAssets >= totalRaised, so grossValue >= principalValue (mulDiv is monotonic).
|
|
234
|
+
uint256 grossValue = Math.mulDiv(shares, accruedTotalAssets + 1, supply + 1, Math.Rounding.Down);
|
|
235
|
+
couponValueGross = grossValue - principalValue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// @dev Vault-wide accrued total assets used to price the holder's slice. Returns flat
|
|
239
|
+
/// `totalRaised` (no coupon) except while the vault is in Lock / PendingSettlement /
|
|
240
|
+
/// SettlementDeadlineExceeded, where the coupon accrues on lock state alone:
|
|
241
|
+
/// - Lock: principal + netInterest * clamp(now - lockStartTime, 0, lockDuration) / lockDuration
|
|
242
|
+
/// - PendingSettlement / SettlementDeadlineExceeded: principal + full netInterest (term is
|
|
243
|
+
/// over; held flat into settlement so there is no Lock-end cliff back to principal)
|
|
244
|
+
/// Accrual deliberately does NOT key on `fundsWithdrawn`. The vault stamps `lockStartTime` at
|
|
245
|
+
/// fundraising close and owes interest over the whole lock window regardless of when, or
|
|
246
|
+
/// whether, the institution draws the principal. Gating the mark on the draw would (a)
|
|
247
|
+
/// under-mark a locked position versus the vault's own settlement and (b) make the mark jump
|
|
248
|
+
/// in the single transaction that flips `fundsWithdrawn` — a sandwichable NAV discontinuity,
|
|
249
|
+
/// since `lockStartTime` (and therefore `elapsed`) is unchanged by the draw. Keying on lock
|
|
250
|
+
/// state alone keeps the mark continuous from `lockStartTime`.
|
|
251
|
+
/// `netInterest` mirrors `BaseVault`: full-term interest then the reserve fee floored and
|
|
252
|
+
/// subtracted (NOT `interest * (1 - reserveFactor)`, which rounds differently).
|
|
253
|
+
function _accruedTotalAssets(IFRVVault vault, IFRVVault.FRVVaultRuntime memory rt) internal view returns (uint256) {
|
|
254
|
+
bool isLock = rt.state == IFRVVault.FRVVaultState.Lock;
|
|
255
|
+
bool isPostTerm = rt.state == IFRVVault.FRVVaultState.PendingSettlement
|
|
256
|
+
|| rt.state == IFRVVault.FRVVaultState.SettlementDeadlineExceeded;
|
|
257
|
+
if (!isLock && !isPostTerm) return rt.totalRaised;
|
|
258
|
+
|
|
259
|
+
IFRVVault.FRVVaultConfig memory cfg = vault.config();
|
|
260
|
+
uint256 lockDuration = cfg.lockDuration;
|
|
261
|
+
if (lockDuration == 0) return rt.totalRaised; // defensive; config enforces non-zero
|
|
262
|
+
|
|
263
|
+
uint256 totalInterest = (rt.totalRaised * cfg.fixedAPY * lockDuration) / (BPS * YEAR);
|
|
264
|
+
// `reserveFactor <= MANTISSA_ONE` is enforced by the FRV vault controller at config time, so
|
|
265
|
+
// the fee never exceeds `totalInterest` and this subtraction cannot underflow.
|
|
266
|
+
uint256 netInterest = totalInterest - (totalInterest * cfg.reserveFactor) / MANTISSA_ONE;
|
|
267
|
+
|
|
268
|
+
uint256 elapsed = lockDuration;
|
|
269
|
+
if (isLock) {
|
|
270
|
+
uint256 raw = block.timestamp > rt.lockStartTime ? block.timestamp - rt.lockStartTime : 0;
|
|
271
|
+
elapsed = raw < lockDuration ? raw : lockDuration;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return rt.totalRaised + (netInterest * elapsed) / lockDuration;
|
|
275
|
+
}
|
|
276
|
+
}
|