@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.
Files changed (76) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +422 -0
  3. package/contracts/Hub/Hub.sol +1614 -0
  4. package/contracts/Hub/HubStorage.sol +106 -0
  5. package/contracts/Hub/lib/HubAdminLib.sol +292 -0
  6. package/contracts/YieldGroup/YieldGroup.sol +128 -0
  7. package/contracts/YieldGroup/YieldGroupFRV.sol +107 -0
  8. package/contracts/YieldGroup/base/YieldGroupBase.sol +797 -0
  9. package/contracts/YieldGroup/base/YieldGroupBaseStorage.sol +73 -0
  10. package/contracts/adapters/AdapterCoreV1.sol +309 -0
  11. package/contracts/adapters/AdapterFRV.sol +276 -0
  12. package/contracts/adapters/AdapterFlux.sol +173 -0
  13. package/contracts/interfaces/IHub.sol +551 -0
  14. package/contracts/interfaces/IHubRegistry.sol +120 -0
  15. package/contracts/interfaces/IMigrator.sol +134 -0
  16. package/contracts/interfaces/IResourceAdapter.sol +154 -0
  17. package/contracts/interfaces/IYieldGroup.sol +119 -0
  18. package/contracts/interfaces/IYieldGroupBase.sol +507 -0
  19. package/contracts/interfaces/IYieldGroupFRV.sol +61 -0
  20. package/contracts/interfaces/external/IComptroller.sol +57 -0
  21. package/contracts/interfaces/external/IFRVVault.sol +113 -0
  22. package/contracts/interfaces/external/IFToken.sol +46 -0
  23. package/contracts/interfaces/external/IFluidLendingResolver.sol +63 -0
  24. package/contracts/interfaces/external/IVBep20.sol +39 -0
  25. package/contracts/interfaces/external/IVToken.sol +125 -0
  26. package/contracts/interfaces/external/IWBNB.sol +16 -0
  27. package/contracts/migrator/Migrator.sol +182 -0
  28. package/contracts/registry/HubRegistry.sol +179 -0
  29. package/deployments/bscmainnet/AdapterCoreV1.json +336 -0
  30. package/deployments/bscmainnet/AdapterFRV.json +225 -0
  31. package/deployments/bscmainnet/AdapterFlux.json +249 -0
  32. package/deployments/bscmainnet/CoreBeacon.json +110 -0
  33. package/deployments/bscmainnet/CoreSource_U.json +1083 -0
  34. package/deployments/bscmainnet/CoreSource_USDC.json +1083 -0
  35. package/deployments/bscmainnet/CoreSource_USDT.json +1083 -0
  36. package/deployments/bscmainnet/FRVBeacon.json +110 -0
  37. package/deployments/bscmainnet/FRVSource_U.json +978 -0
  38. package/deployments/bscmainnet/FRVSource_USDC.json +978 -0
  39. package/deployments/bscmainnet/FRVSource_USDT.json +978 -0
  40. package/deployments/bscmainnet/FluxBeacon.json +110 -0
  41. package/deployments/bscmainnet/FluxSource_U.json +1083 -0
  42. package/deployments/bscmainnet/FluxSource_USDC.json +1083 -0
  43. package/deployments/bscmainnet/FluxSource_USDT.json +1083 -0
  44. package/deployments/bscmainnet/HubBeacon.json +110 -0
  45. package/deployments/bscmainnet/HubImpl.json +2216 -0
  46. package/deployments/bscmainnet/HubRegistry.json +386 -0
  47. package/deployments/bscmainnet/HubRegistryImpl.json +386 -0
  48. package/deployments/bscmainnet/Hub_U.json +2216 -0
  49. package/deployments/bscmainnet/Hub_USDC.json +2216 -0
  50. package/deployments/bscmainnet/Hub_USDT.json +2216 -0
  51. package/deployments/bscmainnet/Migrator.json +375 -0
  52. package/deployments/bscmainnet/YieldGroupCoreImpl.json +1083 -0
  53. package/deployments/bscmainnet/YieldGroupFRVImpl.json +978 -0
  54. package/deployments/bscmainnet/YieldGroupFluxImpl.json +1083 -0
  55. package/deployments/bscmainnet_addresses.json +32 -0
  56. package/deployments/bsctestnet/AdapterCoreV1.json +336 -0
  57. package/deployments/bsctestnet/AdapterFRV.json +225 -0
  58. package/deployments/bsctestnet/AdapterFlux.json +249 -0
  59. package/deployments/bsctestnet/CoreBeacon.json +110 -0
  60. package/deployments/bsctestnet/CoreSource_USDT.json +1083 -0
  61. package/deployments/bsctestnet/FRVBeacon.json +110 -0
  62. package/deployments/bsctestnet/FRVSource_USDT.json +978 -0
  63. package/deployments/bsctestnet/FluxBeacon.json +110 -0
  64. package/deployments/bsctestnet/FluxSource_USDT.json +1083 -0
  65. package/deployments/bsctestnet/HubBeacon.json +110 -0
  66. package/deployments/bsctestnet/HubImpl.json +2216 -0
  67. package/deployments/bsctestnet/HubRegistry.json +386 -0
  68. package/deployments/bsctestnet/HubRegistryImpl.json +386 -0
  69. package/deployments/bsctestnet/HubRegistryProxyAdmin.json +157 -0
  70. package/deployments/bsctestnet/Hub_USDT.json +2216 -0
  71. package/deployments/bsctestnet/Migrator.json +375 -0
  72. package/deployments/bsctestnet/YieldGroupCoreImpl.json +1083 -0
  73. package/deployments/bsctestnet/YieldGroupFRVImpl.json +978 -0
  74. package/deployments/bsctestnet/YieldGroupFluxImpl.json +1083 -0
  75. package/deployments/bsctestnet_addresses.json +25 -0
  76. 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
+ }