@venusprotocol/liquidity-hub 2.1.0-dev.7 → 2.1.0-dev.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -524,6 +524,6 @@ Ordering is guaranteed by the call-array index (`addHub` before any `addYieldGro
524
524
 
525
525
  **Reconciliation:** a Hub is live the moment script 06 runs but discoverable only after its VIP — `deploy/ReconcileHubRegistry.s.sol` (read-only, unnumbered) diffs the committed `deployments/<network>/Hub_*.json` set against `registry.getHubs()` in both directions and reverts on any mismatch (shadow Hubs deployed-but-unregistered, or registered-but-untracked). Run it after every onboarding VIP and periodically from monitoring.
526
526
 
527
- `deploy/config/<network>.json` holds only public addresses and parameters — `acm`, `governance`, `fluxLendingResolver`, `wbnb` / `vBNB` (the Migrator's native path; both keys are required, set them to `0x0` to deploy with that path disabled), and per-asset `key` / `asset` / `name` / `symbol` / `decimalsOffset` / `initialMaxWithdrawalSize` / `blocksPerYear` / `feeRecipient`. The committed `bscmainnet.json` ships with `0x0` placeholders for governance/acm/resolver/recipient and must be filled before a real deploy. Secrets (deployer key, RPC URLs, Etherscan key) live only in `.env` (see `.env.example`). Deployed addresses are committed under `deployments/<network>/<name>.json`; `broadcast/` is gitignored and is not the source of truth.
527
+ `deploy/config/<network>.json` holds only public addresses and parameters — `acm`, `governance`, `fluxLendingResolver`, `wbnb` / `vBNB` (the Migrator's native path; both keys are required and must be non-zero, since the constructor rejects a zero address), and per-asset `key` / `asset` / `name` / `symbol` / `decimalsOffset` / `initialMaxWithdrawalSize` / `blocksPerYear` / `feeRecipient`. The committed `bscmainnet.json` ships with `0x0` placeholders for governance/acm/resolver/recipient and must be filled before a real deploy. Secrets (deployer key, RPC URLs, Etherscan key) live only in `.env` (see `.env.example`). Deployed addresses are committed under `deployments/<network>/<name>.json`; `broadcast/` is gitignored and is not the source of truth.
528
528
 
529
529
  **Networks:** RPC aliases are pre-wired for `bsc_mainnet`, `bsc_testnet`, `ethereum`, `sepolia` (one Etherscan V2 key across chains). v1 targets BNB Chain.
@@ -10,14 +10,15 @@ import { INavGuard } from "../../interfaces/INavGuard.sol";
10
10
  * value is reported by a counterparty that nothing else validates.
11
11
  * @dev The band vouches for a value: the last counterparty figure we accepted, grown at `driftBps`
12
12
  * a year, plus whatever we have moved in or out since. {_guardedNav} holds valuation a gap
13
- * either side of that and never reverts. The gaps are a percentage of the counterparty's part
14
- * only, so our own moves never widen them. Take everything out and the band closes; a deposit
13
+ * either side of that and never reverts. The gaps are a percentage of `anchor`. Our own moves
14
+ * shift only `centre`, so they do not widen the gaps until the next re-anchor, which adopts the
15
+ * whole position as the new `anchor`. Take everything out and the band closes; a deposit
15
16
  * or {setNavGuardSnapshot} reopens it.
16
17
  *
17
18
  * Two stored numbers carry that, and {_currentBand} is the only place they are combined:
18
19
  *
19
- * - `anchor` is the counterparty's part: the last figure they reported that we accepted, less
20
- * anything we have withdrawn beyond our own deposits. Both gaps are sized off it alone, and
20
+ * - `anchor` is what both gaps are sized off: the position value adopted at the last
21
+ * re-anchor, less anything we have withdrawn beyond our own deposits since.
21
22
  * {_updateNavGuard} moves it once per `interval`.
22
23
  * - `centre` is where the band sits: the anchor plus our own deposits since. {_recordInflow}
23
24
  * and {_recordOutflow} move it.
@@ -7,8 +7,10 @@ pragma solidity 0.8.25;
7
7
  * @notice One-click migration from an existing Venus Core position into a Liquidity Hub. In a
8
8
  * single atomic transaction the Migrator pulls a user's Core vToken, redeems it to the
9
9
  * underlying, and deposits that into the matching Hub, minting share tokens to `receiver`.
10
- * @dev A single immutable, permissionless deployment serves every Hub — the target is chosen per
11
- * call via the `hub` argument. Two paths exist because Venus splits its markets by asset type:
10
+ * @dev A single immutable, permissionless deployment serves every registered Hub — the target is
11
+ * chosen per call via the `hub` argument and must be present in the HubRegistry, so a migration
12
+ * can only ever land in a governance-registered Hub. Two paths exist because Venus splits its
13
+ * markets by asset type:
12
14
  * ERC-20-backed markets (vUSDT, vUSDC, ...) go through {migrateFromCore}; the native market
13
15
  * (vBNB) redeems raw BNB and so has the dedicated {migrateFromCoreBNB} path that wraps to WBNB
14
16
  * before depositing into the WBNB Hub.
@@ -36,6 +38,8 @@ interface IMigrator {
36
38
  error ZeroAddress();
37
39
  /// @notice A required amount argument was zero.
38
40
  error ZeroAmount();
41
+ /// @notice The target `hub` is not a governance-registered Hub in the HubRegistry.
42
+ error HubNotRegistered(address hub);
39
43
  /// @notice The vToken's underlying does not match the Hub's asset.
40
44
  error AssetMismatch(address vTokenUnderlying, address hubAsset);
41
45
  /// @notice The Core market returned a non-zero (failure) error code on redeem.
@@ -44,8 +48,6 @@ interface IMigrator {
44
48
  error NothingReceived();
45
49
  /// @notice The minted shares were below the caller's `minShares` floor.
46
50
  error InsufficientShares(uint256 shares, uint256 minShares);
47
- /// @notice The native (vBNB → WBNB) path is unavailable: this deployment has no WBNB/vBNB wired.
48
- error BNBMigrationUnsupported();
49
51
  /// @notice Native value was received from an address other than the configured vBNB market.
50
52
  error OnlyVBNB();
51
53
 
@@ -95,8 +97,8 @@ interface IMigrator {
95
97
  /**
96
98
  * @notice Migrate a native vBNB position into the WBNB Hub.
97
99
  * @dev Caller must first `approve` this Migrator to transfer `vBNBAmount` of the configured
98
- * vBNB market. The redeemed BNB is wrapped to WBNB before deposit. Reverts unless this
99
- * deployment has WBNB/vBNB wired, `IHub(hub).asset() == WBNB`, and `shares >= minShares`.
100
+ * vBNB market. The redeemed BNB is wrapped to WBNB before deposit. Reverts unless `hub` is a
101
+ * registered WBNB Hub (`IHub(hub).asset() == WBNB`) and `shares >= minShares`.
100
102
  * This is the no-consent path — use {migrateFromCoreBNBWithConsent} to record consent.
101
103
  * @param vBNBAmount vBNB receipts to pull and redeem.
102
104
  * @param hub The WBNB Hub to deposit into.
@@ -9,8 +9,9 @@ pragma solidity 0.8.25;
9
9
  * @dev The band vouches for a value: the last counterparty figure we accepted, grown at the
10
10
  * expected rate, plus whatever we have moved in or out since. Valuation is held a gap either
11
11
  * side of that, `min(max(observed, floor), cap)`, and never reverts. The gaps are a percentage
12
- * of the counterparty's part only, so our own moves never widen them. Take everything out and
13
- * the band closes; a deposit or governance reopens it.
12
+ * of `anchor`. Our own moves shift only `centre`, so they do not widen the gaps until the next
13
+ * re-anchor, which adopts the whole position as the new `anchor`. Take everything out and the
14
+ * band closes; a deposit or governance reopens it.
14
15
  *
15
16
  * It bounds value, not price: a position marked as `quantity x price` can be inflated
16
17
  * through either factor. Value is the product plus whatever has no price at all, so one band
@@ -46,9 +47,9 @@ interface INavGuard {
46
47
  * `anchor` and `centre` are stored as of `driftFrom`, and each grows at `driftBps` from
47
48
  * there when read. A closed band has both at zero: every armed side then values the
48
49
  * position at nothing, and only a deposit or {setNavGuardSnapshot} reopens it.
49
- * @param anchor The counterparty's part of the position: the value accepted at the last
50
- * re-anchor, or forced by governance, less what withdrawals have since taken past our own
51
- * deposits. Never above `centre`. Both gaps are sized to it.
50
+ * @param anchor The whole position value accepted at the last re-anchor, or forced by
51
+ * governance, less what withdrawals have since taken past our own deposits. Never above
52
+ * `centre`. Both gaps are sized to it.
52
53
  * @param centre `anchor` plus our own deposits since. What valuation is held around.
53
54
  * @param anchoredAt When `anchor` was last set. `interval` is measured from here, and only a
54
55
  * re-anchor moves it, so our own flows can never delay or bring one forward.
@@ -124,10 +125,10 @@ interface INavGuard {
124
125
 
125
126
  /**
126
127
  * @notice We moved value into or out of a resource, and its centre moved by exactly that. The
127
- * clock is untouched, and the anchor moves only when a withdrawal reaches into the
128
- * counterparty's part.
128
+ * clock is untouched, and the anchor moves only when a withdrawal takes more than our
129
+ * own deposits since the last re-anchor.
129
130
  * @param resource Resource whose centre moved.
130
- * @param anchor The counterparty's part after the move. Zero right after a deposit means the
131
+ * @param anchor The anchor after the move. Zero right after a deposit means the
131
132
  * deposit opened the band, and the band has no width until its first re-anchor.
132
133
  * @param centre New stored centre, before drift.
133
134
  */
@@ -6,6 +6,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s
6
6
  import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
7
7
 
8
8
  import { IHub } from "../interfaces/IHub.sol";
9
+ import { IHubRegistry } from "../interfaces/IHubRegistry.sol";
9
10
  import { IMigrator } from "../interfaces/IMigrator.sol";
10
11
  import { IVBep20 } from "../interfaces/external/IVBep20.sol";
11
12
  import { IWBNB } from "../interfaces/external/IWBNB.sol";
@@ -14,12 +15,16 @@ import { IWBNB } from "../interfaces/external/IWBNB.sol";
14
15
  * @title Migrator
15
16
  * @author Venus
16
17
  * @notice One-click migration from a Venus Core position into a Liquidity Hub. A single immutable,
17
- * permissionless deployment serves every Hub; the target Hub is supplied per call.
18
+ * permissionless deployment serves every registered Hub; the target Hub is supplied per call
19
+ * and must be present in the HubRegistry.
18
20
  * @dev Stateless by design — no proxy, no admin, no funds held between calls. Each migration is
19
21
  * atomic: pull the user's vToken, redeem it to the underlying, deposit that into the Hub, and
20
22
  * mint shares straight to the receiver. If any leg fails the whole call reverts and the user
21
23
  * keeps their vToken.
22
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
+ *
23
28
  * Accounting is done by balance delta, never by a self-reported amount: Core `redeem` returns
24
29
  * a Compound error code (not the redeemed amount) and silently nets out any Comptroller
25
30
  * treasury fee, so the Migrator measures what actually arrived and deposits exactly that. The
@@ -34,16 +39,24 @@ import { IWBNB } from "../interfaces/external/IWBNB.sol";
34
39
  contract Migrator is IMigrator, ReentrancyGuard {
35
40
  using SafeERC20 for IERC20;
36
41
 
37
- /// @notice Canonical wrapped-native token; `address(0)` disables the native path.
42
+ /// @notice Governance-curated directory of valid Hubs.
43
+ IHubRegistry public immutable HUB_REGISTRY;
44
+ /// @notice Canonical wrapped-native token (WBNB).
38
45
  IWBNB public immutable WBNB;
39
- /// @notice Canonical Venus native market (vBNB); `address(0)` disables the native path.
46
+ /// @notice Canonical Venus native market (vBNB).
40
47
  IVBep20 public immutable VBNB;
41
48
 
42
49
  /**
43
- * @param wbnb_ Wrapped-native token, or `address(0)` on chains/deployments with no WBNB Hub.
44
- * @param vbnb_ Venus native market (vBNB), or `address(0)` to leave the native path disabled.
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.
45
54
  */
46
- constructor(IWBNB wbnb_, IVBep20 vbnb_) {
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_;
47
60
  WBNB = wbnb_;
48
61
  VBNB = vbnb_;
49
62
  }
@@ -111,6 +124,7 @@ contract Migrator is IMigrator, ReentrancyGuard {
111
124
  revert ZeroAddress();
112
125
  }
113
126
  if (vTokenAmount == 0) revert ZeroAmount();
127
+ if (!HUB_REGISTRY.isHub(hub)) revert HubNotRegistered(hub);
114
128
 
115
129
  address asset = IHub(hub).asset();
116
130
  address vTokenUnderlying = IVBep20(vToken).underlying();
@@ -135,11 +149,9 @@ contract Migrator is IMigrator, ReentrancyGuard {
135
149
  private
136
150
  returns (uint256 shares)
137
151
  {
138
- if (address(WBNB) == address(0) || address(VBNB) == address(0)) {
139
- revert BNBMigrationUnsupported();
140
- }
141
152
  if (hub == address(0) || receiver == address(0)) revert ZeroAddress();
142
153
  if (vBNBAmount == 0) revert ZeroAmount();
154
+ if (!HUB_REGISTRY.isHub(hub)) revert HubNotRegistered(hub);
143
155
 
144
156
  address asset = IHub(hub).asset();
145
157
  if (asset != address(WBNB)) revert AssetMismatch(address(WBNB), asset);
@@ -1,6 +1,6 @@
1
1
  {
2
- "address": "0xfe6b8BEf1215C19Cd247FbF495ef560932F1Eb9B",
3
- "block": 112781054,
2
+ "address": "0xb0D77DC86d4E6a8282eECF3d4d456B3b94F2a7aD",
3
+ "block": 115268335,
4
4
  "chainId": 56,
5
5
  "contract": "Migrator",
6
6
  "abi": [
@@ -16,6 +16,11 @@
16
16
  "name": "vbnb_",
17
17
  "type": "address",
18
18
  "internalType": "contract IVBep20"
19
+ },
20
+ {
21
+ "name": "hubRegistry_",
22
+ "type": "address",
23
+ "internalType": "contract IHubRegistry"
19
24
  }
20
25
  ],
21
26
  "stateMutability": "nonpayable"
@@ -24,6 +29,19 @@
24
29
  "type": "receive",
25
30
  "stateMutability": "payable"
26
31
  },
32
+ {
33
+ "type": "function",
34
+ "name": "HUB_REGISTRY",
35
+ "inputs": [],
36
+ "outputs": [
37
+ {
38
+ "name": "",
39
+ "type": "address",
40
+ "internalType": "contract IHubRegistry"
41
+ }
42
+ ],
43
+ "stateMutability": "view"
44
+ },
27
45
  {
28
46
  "type": "function",
29
47
  "name": "VBNB",
@@ -292,8 +310,14 @@
292
310
  },
293
311
  {
294
312
  "type": "error",
295
- "name": "BNBMigrationUnsupported",
296
- "inputs": []
313
+ "name": "HubNotRegistered",
314
+ "inputs": [
315
+ {
316
+ "name": "hub",
317
+ "type": "address",
318
+ "internalType": "address"
319
+ }
320
+ ]
297
321
  },
298
322
  {
299
323
  "type": "error",
@@ -30,7 +30,7 @@
30
30
  "Hub_U": "0x0e5AA174d4F31b757a237eb1999DE151596788B0",
31
31
  "Hub_USDC": "0x9D2D9592cF8DFbf59107fAab703d08494BE14617",
32
32
  "Hub_USDT": "0x18AfDACF30F8671021dec4b78297E39d2FE87226",
33
- "Migrator": "0xfe6b8BEf1215C19Cd247FbF495ef560932F1Eb9B",
33
+ "Migrator": "0xb0D77DC86d4E6a8282eECF3d4d456B3b94F2a7aD",
34
34
  "SpokeBeacon": "0x652AAa1CD21f47a92D04680C1d9Bf99cC1Fb8975",
35
35
  "SpokeSource_U": "0xcA3f9AFD7C4c1022E4A37a12671Ef57B8A7bfc51",
36
36
  "SpokeSource_USDC": "0x4cd4aB2F88E39D3F3e481bb2555EcdF00266f717",
@@ -1,6 +1,6 @@
1
1
  {
2
- "address": "0x343D518d8C89f9B5D770000F1ed80f45bF1419f5",
3
- "block": 119460060,
2
+ "address": "0x907d344E53A6e51Cb3E01cb76495D6C5325d72e8",
3
+ "block": 124402350,
4
4
  "chainId": 97,
5
5
  "contract": "Migrator",
6
6
  "abi": [
@@ -16,6 +16,11 @@
16
16
  "name": "vbnb_",
17
17
  "type": "address",
18
18
  "internalType": "contract IVBep20"
19
+ },
20
+ {
21
+ "name": "hubRegistry_",
22
+ "type": "address",
23
+ "internalType": "contract IHubRegistry"
19
24
  }
20
25
  ],
21
26
  "stateMutability": "nonpayable"
@@ -24,6 +29,19 @@
24
29
  "type": "receive",
25
30
  "stateMutability": "payable"
26
31
  },
32
+ {
33
+ "type": "function",
34
+ "name": "HUB_REGISTRY",
35
+ "inputs": [],
36
+ "outputs": [
37
+ {
38
+ "name": "",
39
+ "type": "address",
40
+ "internalType": "contract IHubRegistry"
41
+ }
42
+ ],
43
+ "stateMutability": "view"
44
+ },
27
45
  {
28
46
  "type": "function",
29
47
  "name": "VBNB",
@@ -292,8 +310,14 @@
292
310
  },
293
311
  {
294
312
  "type": "error",
295
- "name": "BNBMigrationUnsupported",
296
- "inputs": []
313
+ "name": "HubNotRegistered",
314
+ "inputs": [
315
+ {
316
+ "name": "hub",
317
+ "type": "address",
318
+ "internalType": "address"
319
+ }
320
+ ]
297
321
  },
298
322
  {
299
323
  "type": "error",
@@ -22,7 +22,7 @@
22
22
  "HubRegistryImpl": "0x4D2C18fB4520c2e4f7C754979e9a4F3BbC1BCe92",
23
23
  "HubRegistryProxyAdmin": "0x9f8413eEE33D434F6D4f40C83181f32A831c9ef7",
24
24
  "Hub_USDT": "0x7cE6ADF754D0eC81A6CF8ACd9C7454F45077dc61",
25
- "Migrator": "0x343D518d8C89f9B5D770000F1ed80f45bF1419f5",
25
+ "Migrator": "0x907d344E53A6e51Cb3E01cb76495D6C5325d72e8",
26
26
  "MockCentrifugeShare_USDT": "0xFF5254510b85D00a4f28fEb850bddF18d6deB043",
27
27
  "MockCentrifugeVault_USDT": "0x8a3C1f806B635230b74741066DE9B0Aa902F83dC",
28
28
  "SpokeBeacon": "0x58099BEaa6A00F493eF67fF953866DDA4a502389",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venusprotocol/liquidity-hub",
3
- "version": "2.1.0-dev.7",
3
+ "version": "2.1.0-dev.8",
4
4
  "description": "Per-asset ERC-4626 allocator vault",
5
5
  "author": "Venus",
6
6
  "license": "BUSL-1.1",