@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,113 @@
|
|
|
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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title IFRVVault
|
|
8
|
+
* @author Venus
|
|
9
|
+
* @notice Minimal interface exposing only what `YieldGroupFRV` needs from a Venus Fixed Rate
|
|
10
|
+
* Vault. The FRV vault is a standard ERC-4626 with a lifecycle state machine on top.
|
|
11
|
+
* @dev The `FRVVaultState` enum, `FRVVaultConfig` and `FRVVaultRuntime` structs are defined inline
|
|
12
|
+
* (not imported from the FRV repo) to avoid an npm dependency. Field order, types and enum
|
|
13
|
+
* numbering MUST match the deployed `VaultState` / `VaultConfig` / `VaultRuntime` ABI exactly —
|
|
14
|
+
* if the FRV repo changes these types, this interface must be updated in lockstep.
|
|
15
|
+
*/
|
|
16
|
+
interface IFRVVault {
|
|
17
|
+
// ──────────────────────────── Types ─────────────────────────────
|
|
18
|
+
|
|
19
|
+
/// @notice Vault lifecycle states. Mirrors `VaultState` in the FRV repo.
|
|
20
|
+
enum FRVVaultState {
|
|
21
|
+
WaitingForMargin, // 0
|
|
22
|
+
MarginDeposited, // 1
|
|
23
|
+
Fundraising, // 2
|
|
24
|
+
InstitutionConfirmation, // 3
|
|
25
|
+
Lock, // 4
|
|
26
|
+
PendingSettlement, // 5
|
|
27
|
+
SettlementDeadlineExceeded, // 6
|
|
28
|
+
Matured, // 7
|
|
29
|
+
Failed, // 8
|
|
30
|
+
Liquidated, // 9
|
|
31
|
+
Closed // 10
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// @notice Immutable vault configuration. Mirrors `VaultConfig` in the FRV repo.
|
|
35
|
+
struct FRVVaultConfig {
|
|
36
|
+
IERC20 supplyAsset;
|
|
37
|
+
uint256 fixedAPY; // basis points (800 = 8%)
|
|
38
|
+
uint256 reserveFactor; // mantissa (0.1e18 = 10%)
|
|
39
|
+
uint256 minBorrowCap;
|
|
40
|
+
uint256 maxBorrowCap;
|
|
41
|
+
uint256 minSupplierDeposit;
|
|
42
|
+
uint40 openDuration;
|
|
43
|
+
uint40 lockDuration;
|
|
44
|
+
uint40 settlementWindow;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// @notice Runtime state that changes as the vault progresses through its lifecycle. Mirrors
|
|
48
|
+
/// `VaultRuntime` in the FRV repo. Field order and types MUST match the deployed ABI
|
|
49
|
+
/// exactly (the timestamps are `uint40`; a struct getter ABI-encodes each field in its
|
|
50
|
+
/// own word, so order/types — not storage packing — are what must line up).
|
|
51
|
+
struct FRVVaultRuntime {
|
|
52
|
+
// ── Lifecycle ──
|
|
53
|
+
FRVVaultState state;
|
|
54
|
+
// ── Timing ──
|
|
55
|
+
uint40 openStartTime;
|
|
56
|
+
uint40 openEndTime;
|
|
57
|
+
uint40 lockStartTime;
|
|
58
|
+
uint40 lockEndTime;
|
|
59
|
+
uint40 settlementDeadline;
|
|
60
|
+
// ── Accounting ──
|
|
61
|
+
uint256 totalRaised;
|
|
62
|
+
uint256 totalDebt;
|
|
63
|
+
uint256 settlementAmount;
|
|
64
|
+
// ── Flags ──
|
|
65
|
+
bool fundsWithdrawn;
|
|
66
|
+
bool protocolShareSettled;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ──────────────────────── ERC-4626 subset ──────────────────────
|
|
70
|
+
|
|
71
|
+
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
|
|
72
|
+
|
|
73
|
+
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
|
|
74
|
+
|
|
75
|
+
/// @notice Vault-wide assets (NOT per-holder). `AdapterFRV` values its own position with
|
|
76
|
+
/// `previewRedeem(balanceOf(holder))` instead, so this is part of the ERC-4626
|
|
77
|
+
/// surface for completeness but is not on the adapter's call path.
|
|
78
|
+
function totalAssets() external view returns (uint256);
|
|
79
|
+
|
|
80
|
+
function maxDeposit(address receiver) external view returns (uint256);
|
|
81
|
+
|
|
82
|
+
function maxWithdraw(address owner) external view returns (uint256);
|
|
83
|
+
|
|
84
|
+
function balanceOf(address account) external view returns (uint256);
|
|
85
|
+
|
|
86
|
+
/// @notice Total vault shares outstanding. Used by `AdapterFRV` to value the holder's slice
|
|
87
|
+
/// pro-rata (`shares / totalSupply`) when accruing the time-based coupon.
|
|
88
|
+
function totalSupply() external view returns (uint256);
|
|
89
|
+
|
|
90
|
+
function previewRedeem(uint256 shares) external view returns (uint256);
|
|
91
|
+
|
|
92
|
+
function asset() external view returns (address);
|
|
93
|
+
|
|
94
|
+
// ─────────────────────── FRV-specific ──────────────────────────
|
|
95
|
+
|
|
96
|
+
/// @notice Current lifecycle state of the vault.
|
|
97
|
+
function state() external view returns (FRVVaultState);
|
|
98
|
+
|
|
99
|
+
/// @notice Immutable vault configuration (APY, caps, timing).
|
|
100
|
+
function config() external view returns (FRVVaultConfig memory);
|
|
101
|
+
|
|
102
|
+
/// @notice Mutable runtime state (lifecycle state, lock window timestamps, accounting, flags).
|
|
103
|
+
/// `AdapterFRV` reads `state` (Lock / PendingSettlement / SettlementDeadlineExceeded gate
|
|
104
|
+
/// the coupon accrual), `lockStartTime` / `lockEndTime` (elapsed-time accrual) and
|
|
105
|
+
/// `totalRaised` (principal base) from this. It does NOT read `fundsWithdrawn`: the mark
|
|
106
|
+
/// accrues on lock state alone, since the vault owes interest over the whole lock window
|
|
107
|
+
/// regardless of when the institution draws.
|
|
108
|
+
function runtime() external view returns (FRVVaultRuntime memory);
|
|
109
|
+
|
|
110
|
+
/// @notice Permissionless state-machine advance. Triggers time-based transitions
|
|
111
|
+
/// (e.g. Fundraising → Lock, PendingSettlement → Matured).
|
|
112
|
+
function updateVaultState() external;
|
|
113
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title IFToken
|
|
6
|
+
* @author Venus
|
|
7
|
+
* @notice Minimal interface exposing only what `AdapterFlux` needs from a Fluid lending fToken.
|
|
8
|
+
* A Fluid fToken is a standard ERC-4626 share token that forwards every deposit to the
|
|
9
|
+
* Fluid Liquidity layer and only mints / burns shares — it never custodies the underlying.
|
|
10
|
+
* @dev Declared locally (not imported from the Fluid repo) to avoid an external dependency, the
|
|
11
|
+
* same approach `IFRVVault` takes. The selectors below are the canonical ERC-4626 ones, so
|
|
12
|
+
* they bind to any compliant fToken regardless of the Fluid repo's compiler version.
|
|
13
|
+
*
|
|
14
|
+
* Behaviour notes that matter to the adapter:
|
|
15
|
+
* - `deposit` pulls `assets` from `msg.sender` via the fToken's `liquidityCallback`, so the
|
|
16
|
+
* caller (the YieldGroup, under delegatecall) must have approved the fToken first.
|
|
17
|
+
* - `withdraw` delivers exactly `assets` to `receiver` and burns shares from `owner`; no
|
|
18
|
+
* redeem-time fee (unlike a Compound-style treasury cut).
|
|
19
|
+
* - `maxDeposit` returns headroom up to `int128.max` (effectively unbounded); the real
|
|
20
|
+
* deposit control for Flux is the YieldGroup's per-resource cap.
|
|
21
|
+
* - `maxWithdraw` is `min(Fluid withdrawal-limit, owner balance)`.
|
|
22
|
+
*/
|
|
23
|
+
interface IFToken {
|
|
24
|
+
// ──────────────────────── ERC-4626 subset ──────────────────────
|
|
25
|
+
|
|
26
|
+
/// @notice Deposit `assets` of the underlying, minting fToken shares to `receiver`.
|
|
27
|
+
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
|
|
28
|
+
|
|
29
|
+
/// @notice Withdraw exactly `assets` of the underlying to `receiver`, burning `owner`'s shares.
|
|
30
|
+
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
|
|
31
|
+
|
|
32
|
+
/// @notice The ERC-20 underlying this fToken accepts.
|
|
33
|
+
function asset() external view returns (address);
|
|
34
|
+
|
|
35
|
+
/// @notice fToken share balance of `account`.
|
|
36
|
+
function balanceOf(address account) external view returns (uint256);
|
|
37
|
+
|
|
38
|
+
/// @notice Underlying value of `shares` at the current exchange price (incl. rewards).
|
|
39
|
+
function previewRedeem(uint256 shares) external view returns (uint256);
|
|
40
|
+
|
|
41
|
+
/// @notice Spare deposit headroom (≈ `int128.max − totalSupply`; effectively unbounded).
|
|
42
|
+
function maxDeposit(address receiver) external view returns (uint256);
|
|
43
|
+
|
|
44
|
+
/// @notice Currently-withdrawable underlying for `owner`: `min(Fluid limit, owner balance)`.
|
|
45
|
+
function maxWithdraw(address owner) external view returns (uint256);
|
|
46
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title IFluidLendingResolver
|
|
6
|
+
* @author Venus
|
|
7
|
+
* @notice Minimal interface for the Fluid periphery `LendingResolver` — a read-only contract
|
|
8
|
+
* Fluid deploys and maintains (e.g. `0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569` on BSC).
|
|
9
|
+
* `AdapterFlux` calls `getFTokenDetails` to read an fToken's spot supply APR and reward
|
|
10
|
+
* rate; the fToken itself exposes only cumulative exchange prices, not a spot rate, so the
|
|
11
|
+
* resolver (which runs Fluid's `LiquidityCalcs` math for us) is the clean yieldGroup.
|
|
12
|
+
* @dev The `FTokenDetails` and nested `UserSupplyData` structs are mirrored VERBATIM from Fluid's
|
|
13
|
+
* `periphery/resolvers/lending/structs.sol` and `periphery/resolvers/liquidity/structs.sol`.
|
|
14
|
+
* Field order and types MUST match the deployed resolver's return ABI exactly — a mismatch
|
|
15
|
+
* silently corrupts the decode. If Fluid revs the resolver struct, update this in lockstep
|
|
16
|
+
* (same "must match ABI" contract as `IFRVVault`). The function is declared with an `address`
|
|
17
|
+
* parameter so its selector is `getFTokenDetails(address)`, identical to the deployed one.
|
|
18
|
+
*
|
|
19
|
+
* Only `supplyRate` (1e2 precision, i.e. already basis points) and `rewardsRate`
|
|
20
|
+
* (1e12 precision, `/1e10` to convert to bps) are consumed by the adapter; the remaining
|
|
21
|
+
* fields exist purely so the tuple decodes correctly.
|
|
22
|
+
*/
|
|
23
|
+
interface IFluidLendingResolver {
|
|
24
|
+
/// @notice Mirror of Fluid liquidity resolver `UserSupplyData`. All fields are value types.
|
|
25
|
+
struct UserSupplyData {
|
|
26
|
+
bool modeWithInterest;
|
|
27
|
+
uint256 supply;
|
|
28
|
+
uint256 withdrawalLimit;
|
|
29
|
+
uint256 lastUpdateTimestamp;
|
|
30
|
+
uint256 expandPercent;
|
|
31
|
+
uint256 expandDuration;
|
|
32
|
+
uint256 baseWithdrawalLimit;
|
|
33
|
+
uint256 withdrawableUntilLimit;
|
|
34
|
+
uint256 withdrawable;
|
|
35
|
+
uint256 decayEndTimestamp;
|
|
36
|
+
uint256 decayAmount;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// @notice Mirror of Fluid lending resolver `FTokenDetails`. `rewardsRate` precedes
|
|
40
|
+
/// `supplyRate`; both are the only fields the adapter reads.
|
|
41
|
+
struct FTokenDetails {
|
|
42
|
+
address tokenAddress;
|
|
43
|
+
bool eip2612Deposits;
|
|
44
|
+
bool isNativeUnderlying;
|
|
45
|
+
string name;
|
|
46
|
+
string symbol;
|
|
47
|
+
uint256 decimals;
|
|
48
|
+
address asset;
|
|
49
|
+
uint256 totalAssets;
|
|
50
|
+
uint256 totalSupply;
|
|
51
|
+
uint256 convertToShares;
|
|
52
|
+
uint256 convertToAssets;
|
|
53
|
+
uint256 rewardsRate; // additional yield from rewards, in 1e12 precision (1e12 = 1%)
|
|
54
|
+
uint256 supplyRate; // base Liquidity yield, in 1e2 precision (100 = 1% = 100 bps)
|
|
55
|
+
int256 rebalanceDifference;
|
|
56
|
+
UserSupplyData liquidityUserSupplyData;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// @notice All public details for `fToken`, including spot `supplyRate` and `rewardsRate`.
|
|
60
|
+
/// @param fToken The fToken to read.
|
|
61
|
+
/// @return fTokenDetails Populated {FTokenDetails}.
|
|
62
|
+
function getFTokenDetails(address fToken) external view returns (FTokenDetails memory fTokenDetails);
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title IVBep20
|
|
8
|
+
* @author Venus
|
|
9
|
+
* @notice Narrow view of a Venus Core market (`VBep20`) as the Migrator consumes it: the
|
|
10
|
+
* transferable ERC-20 receipt plus the two accessors the Migrator needs to redeem a
|
|
11
|
+
* position back to its underlying.
|
|
12
|
+
* @dev Conventions mirror the live Core contracts:
|
|
13
|
+
* - The receipt is a full ERC-20, so a position is pulled in with `safeTransferFrom`. Note
|
|
14
|
+
* the Core `transferFrom` returns `false` (it does not revert) on failure, so callers must
|
|
15
|
+
* route it through `SafeERC20`.
|
|
16
|
+
* - `redeem` follows Compound semantics: it returns an error code (`0` on success) AND can
|
|
17
|
+
* revert (the Comptroller `redeemAllowed` hook, insufficient cash, and block-freshness
|
|
18
|
+
* checks all revert). Callers must both check the returned code and verify a balance delta;
|
|
19
|
+
* `redeem` never reports the redeemed amount through its return value.
|
|
20
|
+
* - `underlying()` exists only on ERC-20-backed markets; the native market (vBNB) has no such
|
|
21
|
+
* accessor, so a call to it reverts — which the Migrator relies on to reject vBNB from the
|
|
22
|
+
* ERC-20 path and force the dedicated native path.
|
|
23
|
+
*/
|
|
24
|
+
interface IVBep20 is IERC20 {
|
|
25
|
+
/**
|
|
26
|
+
* @notice Burn `redeemTokens` vToken receipts from `msg.sender` and send the corresponding
|
|
27
|
+
* underlying (net of any Comptroller treasury fee) to `msg.sender`.
|
|
28
|
+
* @param redeemTokens vToken units to burn.
|
|
29
|
+
* @return errorCode Compound-style error code; `0` on success.
|
|
30
|
+
*/
|
|
31
|
+
function redeem(uint256 redeemTokens) external returns (uint256 errorCode);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @notice The ERC-20 underlying this market is backed by.
|
|
35
|
+
* @dev Reverts for the native (vBNB) market, which has no underlying accessor.
|
|
36
|
+
* @return token Address of the underlying ERC-20.
|
|
37
|
+
*/
|
|
38
|
+
function underlying() external view returns (address token);
|
|
39
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.25;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title IVToken
|
|
6
|
+
* @author Venus
|
|
7
|
+
* @notice Minimal subset of the Venus Core `VBep20` interface needed by the Core Yield Group
|
|
8
|
+
* adapter. Kept narrow on purpose: importing Venus's full `VBep20Interface` drags in
|
|
9
|
+
* Comptroller diamond storage and a dozen transitive imports.
|
|
10
|
+
* @dev Conventions:
|
|
11
|
+
* - Mutating calls follow Compound semantics: return value is an error code (0 = success,
|
|
12
|
+
* non-zero = failure). Cap and pause violations REVERT inside the Comptroller hook
|
|
13
|
+
* (PolicyFacet) rather than returning a non-zero code — the adapter must handle both
|
|
14
|
+
* revert and non-zero-return failure modes.
|
|
15
|
+
* - `mint(amount)` pulls `amount` underlying from `msg.sender` via `transferFrom` and
|
|
16
|
+
* credits `msg.sender` with vTokens. For fee-on-transfer underlyings the actual minted
|
|
17
|
+
* amount equals what was received (less than requested) — not supported by the
|
|
18
|
+
* Core Yield Group v1.
|
|
19
|
+
* - `redeemUnderlying(amount)` burns vTokens from `msg.sender` and sends underlying to
|
|
20
|
+
* `msg.sender`. When the Comptroller's `treasuryPercent() != 0`, the receiver gets
|
|
21
|
+
* `amount − (amount × treasuryPercent / 1e18)` and the difference is sent to the
|
|
22
|
+
* treasury. Adapters must gross-up to deliver an exact net amount.
|
|
23
|
+
* - `mintBehalf` / `redeemUnderlyingBehalf` are the "on behalf of another address"
|
|
24
|
+
* variants. NOTE: `AdapterCoreV1` does NOT use them — it dispatches via DELEGATECALL and
|
|
25
|
+
* calls plain `mint` / `redeemUnderlying`, so `msg.sender` already is the YieldGroup and
|
|
26
|
+
* vTokens land on it directly (no delegate approval needed). These Behalf selectors, and
|
|
27
|
+
* the `Comptroller.updateDelegate` / `approvedDelegates` pair, are retained only for
|
|
28
|
+
* reference and the integration mocks; they are on no production call path.
|
|
29
|
+
* - `balanceOfUnderlying` and `exchangeRateCurrent` are intentionally OMITTED here: they
|
|
30
|
+
* mutate state (accrue interest) and have no place in a view-only adapter accessor.
|
|
31
|
+
*/
|
|
32
|
+
interface IVToken {
|
|
33
|
+
// -------------------------------- Mutating -------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @notice Supply `mintAmount` of underlying and receive vTokens.
|
|
37
|
+
* @param mintAmount Underlying units to deposit.
|
|
38
|
+
* @return errorCode Compound-style error code; `0` on success.
|
|
39
|
+
*/
|
|
40
|
+
function mint(uint256 mintAmount) external returns (uint256 errorCode);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @notice Burn vTokens and receive `redeemAmount` of underlying (less any treasury fee).
|
|
44
|
+
* @param redeemAmount Underlying units to withdraw (gross of any treasury fee).
|
|
45
|
+
* @return errorCode Compound-style error code; `0` on success.
|
|
46
|
+
*/
|
|
47
|
+
function redeemUnderlying(uint256 redeemAmount) external returns (uint256 errorCode);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @notice Supply `mintAmount` of underlying on behalf of `receiver`. Pulls underlying
|
|
51
|
+
* from `msg.sender` (the caller) and credits vTokens to `receiver`. No delegate
|
|
52
|
+
* approval required — anyone can mint to anyone; the payer just provides funds.
|
|
53
|
+
* @param receiver Address that will be credited with vTokens.
|
|
54
|
+
* @param mintAmount Underlying units to deposit.
|
|
55
|
+
* @return errorCode Compound-style error code; `0` on success.
|
|
56
|
+
*/
|
|
57
|
+
function mintBehalf(address receiver, uint256 mintAmount) external returns (uint256 errorCode);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @notice Redeem `redeemAmount` of underlying on behalf of `redeemer`. Burns vTokens
|
|
61
|
+
* from `redeemer` and sends underlying to `msg.sender` (less any treasury fee).
|
|
62
|
+
* Requires `redeemer` to have approved `msg.sender` as a delegate via
|
|
63
|
+
* `Comptroller.updateDelegate(msg.sender, true)`.
|
|
64
|
+
* @param redeemer vToken holder whose receipts will be burned.
|
|
65
|
+
* @param redeemAmount Underlying units to withdraw (gross of any treasury fee).
|
|
66
|
+
* @return errorCode Compound-style error code; `0` on success.
|
|
67
|
+
*/
|
|
68
|
+
function redeemUnderlyingBehalf(address redeemer, uint256 redeemAmount) external returns (uint256 errorCode);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @notice Settle accrued interest into the stored exchange rate up to the current block.
|
|
72
|
+
* @return errorCode Compound-style error code; `0` on success.
|
|
73
|
+
*/
|
|
74
|
+
function accrueInterest() external returns (uint256 errorCode);
|
|
75
|
+
|
|
76
|
+
// ---------------------------------- Views --------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @notice vToken balance of `account` (NOT underlying units; multiply by
|
|
80
|
+
* `exchangeRateStored()` and divide by `1e18` for underlying value).
|
|
81
|
+
* @param account Holder to query.
|
|
82
|
+
* @return balance vToken units held by `account`.
|
|
83
|
+
*/
|
|
84
|
+
function balanceOf(address account) external view returns (uint256 balance);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @notice Last-cached underlying-per-vToken exchange rate, scaled by `1e18`.
|
|
88
|
+
* @dev Stale by up to one accrual cycle. Cheap; suitable for view-only adapter accessors.
|
|
89
|
+
* The mutating variant `exchangeRateCurrent` accrues interest as a side effect and
|
|
90
|
+
* should not be called from view paths.
|
|
91
|
+
* @return rate Exchange rate scaled by `1e18`.
|
|
92
|
+
*/
|
|
93
|
+
function exchangeRateStored() external view returns (uint256 rate);
|
|
94
|
+
|
|
95
|
+
/// @notice Total vToken supply (in vToken units, not underlying).
|
|
96
|
+
/// @return supply Total outstanding vTokens.
|
|
97
|
+
function totalSupply() external view returns (uint256 supply);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @notice Underlying cash currently held by the vToken contract.
|
|
101
|
+
* @dev This is the realistic upper bound on a single `redeemUnderlying` call: the vToken
|
|
102
|
+
* cannot pay out more underlying than it holds in cash.
|
|
103
|
+
* @return cash Underlying units held by the vToken.
|
|
104
|
+
*/
|
|
105
|
+
function getCash() external view returns (uint256 cash);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @notice The ERC-20 underlying token. Reverts for vBNB (which has no `underlying`
|
|
109
|
+
* accessor); the Core Yield Group v1 uses this revert as a guard against vBNB
|
|
110
|
+
* being registered as a resource.
|
|
111
|
+
* @return token Address of the underlying ERC-20.
|
|
112
|
+
*/
|
|
113
|
+
function underlying() external view returns (address token);
|
|
114
|
+
|
|
115
|
+
/// @notice Comptroller controlling this vToken.
|
|
116
|
+
/// @return comptrollerAddress Address of the Comptroller.
|
|
117
|
+
function comptroller() external view returns (address comptrollerAddress);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @notice Current per-block supply interest rate, scaled by `1e18`.
|
|
121
|
+
* @dev Annualise by multiplying by the deployed `InterestRateModel`'s `blocksPerYear`.
|
|
122
|
+
* @return rate Per-block supply rate, scaled by `1e18`.
|
|
123
|
+
*/
|
|
124
|
+
function supplyRatePerBlock() external view returns (uint256 rate);
|
|
125
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title IWBNB
|
|
8
|
+
* @author Venus
|
|
9
|
+
* @notice Minimal view of the canonical wrapped-native token (WBNB). The Migrator only needs to
|
|
10
|
+
* wrap native BNB it receives from a vBNB redeem, then hand the WBNB to a Hub, so this
|
|
11
|
+
* exposes `deposit()` plus the ERC-20 surface (for the `forceApprove` to the Hub).
|
|
12
|
+
*/
|
|
13
|
+
interface IWBNB is IERC20 {
|
|
14
|
+
/// @notice Wrap the attached native value 1:1 into WBNB credited to `msg.sender`.
|
|
15
|
+
function deposit() external payable;
|
|
16
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
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 { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
7
|
+
|
|
8
|
+
import { IHub } from "../interfaces/IHub.sol";
|
|
9
|
+
import { IHubRegistry } from "../interfaces/IHubRegistry.sol";
|
|
10
|
+
import { IMigrator } from "../interfaces/IMigrator.sol";
|
|
11
|
+
import { IVBep20 } from "../interfaces/external/IVBep20.sol";
|
|
12
|
+
import { IWBNB } from "../interfaces/external/IWBNB.sol";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @title Migrator
|
|
16
|
+
* @author Venus
|
|
17
|
+
* @notice One-click migration from a Venus Core position into a Liquidity Hub. A single immutable,
|
|
18
|
+
* permissionless deployment serves every registered Hub; the target Hub is supplied per call
|
|
19
|
+
* and must be present in the HubRegistry.
|
|
20
|
+
* @dev Stateless by design — no proxy, no admin, no funds held between calls. Each migration is
|
|
21
|
+
* atomic: pull the user's vToken, redeem it to the underlying, deposit that into the Hub, and
|
|
22
|
+
* mint shares straight to the receiver. If any leg fails the whole call reverts and the user
|
|
23
|
+
* keeps their vToken.
|
|
24
|
+
*
|
|
25
|
+
* The target `hub` is checked against the immutable {HUB_REGISTRY} before any funds move, so a
|
|
26
|
+
* migration can only land in a Hub governance has registered.
|
|
27
|
+
*
|
|
28
|
+
* Accounting is done by balance delta, never by a self-reported amount: Core `redeem` returns
|
|
29
|
+
* a Compound error code (not the redeemed amount) and silently nets out any Comptroller
|
|
30
|
+
* treasury fee, so the Migrator measures what actually arrived and deposits exactly that. The
|
|
31
|
+
* caller's `minShares` is the slippage guard.
|
|
32
|
+
*
|
|
33
|
+
* Two paths mirror how Venus splits its markets. ERC-20-backed markets (vUSDT, vUSDC, ...) use
|
|
34
|
+
* {migrateFromCore}, which validates `vToken.underlying() == hub.asset()`. The native market
|
|
35
|
+
* (vBNB) has no `underlying()` accessor and redeems raw BNB, so {migrateFromCoreBNB} wraps the
|
|
36
|
+
* BNB to WBNB before depositing into the WBNB Hub. vBNB pays out via `transfer` (a 2300-gas
|
|
37
|
+
* stipend), so {receive} stays minimal and the wrapping happens in the function body.
|
|
38
|
+
*/
|
|
39
|
+
contract Migrator is IMigrator, ReentrancyGuard {
|
|
40
|
+
using SafeERC20 for IERC20;
|
|
41
|
+
|
|
42
|
+
/// @notice Governance-curated directory of valid Hubs.
|
|
43
|
+
IHubRegistry public immutable HUB_REGISTRY;
|
|
44
|
+
/// @notice Canonical wrapped-native token (WBNB).
|
|
45
|
+
IWBNB public immutable WBNB;
|
|
46
|
+
/// @notice Canonical Venus native market (vBNB).
|
|
47
|
+
IVBep20 public immutable VBNB;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param wbnb_ Wrapped-native token (WBNB). Mandatory; the constructor reverts on the zero address.
|
|
51
|
+
* @param vbnb_ Venus native market (vBNB). Mandatory; the constructor reverts on the zero address.
|
|
52
|
+
* @param hubRegistry_ The HubRegistry every migration target is validated against. Mandatory;
|
|
53
|
+
* the constructor reverts on the zero address, since without it no Hub would be accepted.
|
|
54
|
+
*/
|
|
55
|
+
constructor(IWBNB wbnb_, IVBep20 vbnb_, IHubRegistry hubRegistry_) {
|
|
56
|
+
if (address(wbnb_) == address(0) || address(vbnb_) == address(0) || address(hubRegistry_) == address(0)) {
|
|
57
|
+
revert ZeroAddress();
|
|
58
|
+
}
|
|
59
|
+
HUB_REGISTRY = hubRegistry_;
|
|
60
|
+
WBNB = wbnb_;
|
|
61
|
+
VBNB = vbnb_;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// @notice Accept native BNB only from the configured vBNB market (the redeem payout).
|
|
65
|
+
receive() external payable {
|
|
66
|
+
if (msg.sender != address(VBNB)) revert OnlyVBNB();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// @inheritdoc IMigrator
|
|
70
|
+
function migrateFromCore(address vToken, uint256 vTokenAmount, address hub, address receiver, uint256 minShares)
|
|
71
|
+
external
|
|
72
|
+
nonReentrant
|
|
73
|
+
returns (uint256 shares)
|
|
74
|
+
{
|
|
75
|
+
shares = _migrateFromCore(vToken, vTokenAmount, hub, receiver, minShares);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// @inheritdoc IMigrator
|
|
79
|
+
function migrateFromCoreWithConsent(
|
|
80
|
+
address vToken,
|
|
81
|
+
uint256 vTokenAmount,
|
|
82
|
+
address hub,
|
|
83
|
+
address receiver,
|
|
84
|
+
uint256 minShares,
|
|
85
|
+
bytes32 consentHash
|
|
86
|
+
) external nonReentrant returns (uint256 shares) {
|
|
87
|
+
// Record the migrating user's consent before any state change, so it rolls back with the tx if
|
|
88
|
+
// a later leg reverts. Attributed to msg.sender: the user calls this Migrator directly, so it
|
|
89
|
+
// is the authenticated consenting party even though the Hub deposit is intermediated. The hash
|
|
90
|
+
// is optional — bytes32(0) skips the emit and migrates without recording.
|
|
91
|
+
if (consentHash != bytes32(0)) emit IHub.ConsentRecorded(msg.sender, receiver, consentHash);
|
|
92
|
+
shares = _migrateFromCore(vToken, vTokenAmount, hub, receiver, minShares);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// @inheritdoc IMigrator
|
|
96
|
+
function migrateFromCoreBNB(uint256 vBNBAmount, address hub, address receiver, uint256 minShares)
|
|
97
|
+
external
|
|
98
|
+
nonReentrant
|
|
99
|
+
returns (uint256 shares)
|
|
100
|
+
{
|
|
101
|
+
shares = _migrateFromCoreBNB(vBNBAmount, hub, receiver, minShares);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// @inheritdoc IMigrator
|
|
105
|
+
function migrateFromCoreBNBWithConsent(
|
|
106
|
+
uint256 vBNBAmount,
|
|
107
|
+
address hub,
|
|
108
|
+
address receiver,
|
|
109
|
+
uint256 minShares,
|
|
110
|
+
bytes32 consentHash
|
|
111
|
+
) external nonReentrant returns (uint256 shares) {
|
|
112
|
+
// Record the migrating user's consent before any state change (attributed to msg.sender);
|
|
113
|
+
// rolled back with the tx if a later leg reverts. bytes32(0) skips the emit.
|
|
114
|
+
if (consentHash != bytes32(0)) emit IHub.ConsentRecorded(msg.sender, receiver, consentHash);
|
|
115
|
+
shares = _migrateFromCoreBNB(vBNBAmount, hub, receiver, minShares);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// @dev Shared body for {migrateFromCore} / {migrateFromCoreWithConsent}: pull, redeem, deposit.
|
|
119
|
+
function _migrateFromCore(address vToken, uint256 vTokenAmount, address hub, address receiver, uint256 minShares)
|
|
120
|
+
private
|
|
121
|
+
returns (uint256 shares)
|
|
122
|
+
{
|
|
123
|
+
if (vToken == address(0) || hub == address(0) || receiver == address(0)) {
|
|
124
|
+
revert ZeroAddress();
|
|
125
|
+
}
|
|
126
|
+
if (vTokenAmount == 0) revert ZeroAmount();
|
|
127
|
+
if (!HUB_REGISTRY.isHub(hub)) revert HubNotRegistered(hub);
|
|
128
|
+
|
|
129
|
+
address asset = IHub(hub).asset();
|
|
130
|
+
address vTokenUnderlying = IVBep20(vToken).underlying();
|
|
131
|
+
if (vTokenUnderlying != asset) revert AssetMismatch(vTokenUnderlying, asset);
|
|
132
|
+
|
|
133
|
+
// Pull the receipt, then redeem it for ourselves (we now own the vTokens).
|
|
134
|
+
IERC20(vToken).safeTransferFrom(msg.sender, address(this), vTokenAmount);
|
|
135
|
+
|
|
136
|
+
uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
|
|
137
|
+
uint256 errorCode = IVBep20(vToken).redeem(vTokenAmount);
|
|
138
|
+
if (errorCode != 0) revert VTokenRedeemFailed(vToken, errorCode);
|
|
139
|
+
uint256 underlyingAmount = IERC20(asset).balanceOf(address(this)) - balanceBefore;
|
|
140
|
+
if (underlyingAmount == 0) revert NothingReceived();
|
|
141
|
+
|
|
142
|
+
shares = _depositToHub(IERC20(asset), hub, underlyingAmount, receiver, minShares);
|
|
143
|
+
emit Migrated(msg.sender, vToken, hub, vTokenAmount, underlyingAmount, shares);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// @dev Shared body for {migrateFromCoreBNB} / {migrateFromCoreBNBWithConsent}: pull, redeem,
|
|
147
|
+
/// wrap to WBNB, deposit.
|
|
148
|
+
function _migrateFromCoreBNB(uint256 vBNBAmount, address hub, address receiver, uint256 minShares)
|
|
149
|
+
private
|
|
150
|
+
returns (uint256 shares)
|
|
151
|
+
{
|
|
152
|
+
if (hub == address(0) || receiver == address(0)) revert ZeroAddress();
|
|
153
|
+
if (vBNBAmount == 0) revert ZeroAmount();
|
|
154
|
+
if (!HUB_REGISTRY.isHub(hub)) revert HubNotRegistered(hub);
|
|
155
|
+
|
|
156
|
+
address asset = IHub(hub).asset();
|
|
157
|
+
if (asset != address(WBNB)) revert AssetMismatch(address(WBNB), asset);
|
|
158
|
+
|
|
159
|
+
IERC20(address(VBNB)).safeTransferFrom(msg.sender, address(this), vBNBAmount);
|
|
160
|
+
|
|
161
|
+
uint256 balanceBefore = address(this).balance;
|
|
162
|
+
uint256 errorCode = VBNB.redeem(vBNBAmount);
|
|
163
|
+
if (errorCode != 0) revert VTokenRedeemFailed(address(VBNB), errorCode);
|
|
164
|
+
uint256 nativeAmount = address(this).balance - balanceBefore;
|
|
165
|
+
if (nativeAmount == 0) revert NothingReceived();
|
|
166
|
+
|
|
167
|
+
WBNB.deposit{ value: nativeAmount }();
|
|
168
|
+
shares = _depositToHub(IERC20(address(WBNB)), hub, nativeAmount, receiver, minShares);
|
|
169
|
+
emit Migrated(msg.sender, address(VBNB), hub, vBNBAmount, nativeAmount, shares);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// @dev Approve `hub` for exactly `amount`, deposit to `receiver`, and enforce the slippage
|
|
173
|
+
/// floor. `deposit` pulls the full `amount`, so no allowance or balance is left behind.
|
|
174
|
+
function _depositToHub(IERC20 asset, address hub, uint256 amount, address receiver, uint256 minShares)
|
|
175
|
+
private
|
|
176
|
+
returns (uint256 shares)
|
|
177
|
+
{
|
|
178
|
+
asset.forceApprove(hub, amount);
|
|
179
|
+
shares = IHub(hub).deposit(amount, receiver);
|
|
180
|
+
if (shares < minShares) revert InsufficientShares(shares, minShares);
|
|
181
|
+
}
|
|
182
|
+
}
|