From 11a41643f2f3199b550563c647623c7fd3e6f19c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 8 Jul 2026 19:15:55 +0530 Subject: [PATCH 1/3] feat: add VIP Liquidity Hub (USDT) testnet wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Grant the Hub's asymmetric ACM roles: the full governance set to the Normal timelock (which then runs the wiring) plus the Guardian operator set; a companion addendum grants governance to the Fast-Track and Critical timelocks. - Wire the Core yield source end-to-end (register vUSDT behind AdapterCoreV1, set inner and outer queues) so the USDT Hub routes deposits and withdrawals through the Core pool. - Split the grants into a main proposal plus bsctestnet-addendum.ts: all three timelocks' grants plus the wiring exceed BSC's per-tx propose gas cap in a single proposal. - Defer the FRV and Flux sources — neither has a concrete resource on testnet yet (no FRV vault instance; Flux adapter not deployed). - Add fork simulations for both proposals: pre/post state, the 40 and 58 role grants, and an end-to-end maxDeposit check. --- .../vip-680/abi/AccessControlManager.json | 360 +++ simulations/vip-680/abi/Hub.json | 2127 +++++++++++++++++ simulations/vip-680/abi/YieldGroup.json | 1077 +++++++++ simulations/vip-680/bsctestnet-addendum.ts | 95 + simulations/vip-680/bsctestnet.ts | 201 ++ vips/vip-680/bsctestnet-addendum.ts | 65 + vips/vip-680/bsctestnet.ts | 205 ++ 7 files changed, 4130 insertions(+) create mode 100644 simulations/vip-680/abi/AccessControlManager.json create mode 100644 simulations/vip-680/abi/Hub.json create mode 100644 simulations/vip-680/abi/YieldGroup.json create mode 100644 simulations/vip-680/bsctestnet-addendum.ts create mode 100644 simulations/vip-680/bsctestnet.ts create mode 100644 vips/vip-680/bsctestnet-addendum.ts create mode 100644 vips/vip-680/bsctestnet.ts diff --git a/simulations/vip-680/abi/AccessControlManager.json b/simulations/vip-680/abi/AccessControlManager.json new file mode 100644 index 000000000..4a118fcc4 --- /dev/null +++ b/simulations/vip-680/abi/AccessControlManager.json @@ -0,0 +1,360 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "functionSig", + "type": "string" + } + ], + "name": "PermissionGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "functionSig", + "type": "string" + } + ], + "name": "PermissionRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "functionSig", + "type": "string" + }, + { + "internalType": "address", + "name": "accountToPermit", + "type": "address" + } + ], + "name": "giveCallPermission", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "functionSig", + "type": "string" + } + ], + "name": "hasPermission", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "string", + "name": "functionSig", + "type": "string" + } + ], + "name": "isAllowedToCall", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "functionSig", + "type": "string" + }, + { + "internalType": "address", + "name": "accountToRevoke", + "type": "address" + } + ], + "name": "revokeCallPermission", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-680/abi/Hub.json b/simulations/vip-680/abi/Hub.json new file mode 100644 index 000000000..7867cd865 --- /dev/null +++ b/simulations/vip-680/abi/Hub.json @@ -0,0 +1,2127 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "BPS_DENOMINATOR", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "EXP_SCALE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MAX_DECIMALS_OFFSET", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MAX_FEE_BPS", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MAX_MGMT_FEE_PER_ACCRUAL_BPS", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MAX_REDEEM_FEE_BPS", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "SECONDS_PER_YEAR", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "accessControlManager", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IAccessControlManagerV8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "accrueFees", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addYieldGroup", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "absoluteCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "percentageCapBps", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "asset", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToAssets", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToShares", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decreaseAllowance", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "subtractedValue", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "emergencyReallocate", + "inputs": [ + { + "name": "withdraws", + "type": "tuple[]", + "internalType": "struct IHub.ReallocateLeg[]", + "components": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "deposits", + "type": "tuple[]", + "internalType": "struct IHub.ReallocateLeg[]", + "components": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "feeBps", + "inputs": [], + "outputs": [ + { + "name": "managementBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "performanceBps", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "feeRecipient", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "highWaterMarkPerShare", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "hubPaused", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "increaseAllowance", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "addedValue", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "asset_", + "type": "address", + "internalType": "contract IERC20Upgradeable" + }, + { + "name": "name_", + "type": "string", + "internalType": "string" + }, + { + "name": "symbol_", + "type": "string", + "internalType": "string" + }, + { + "name": "acm_", + "type": "address", + "internalType": "address" + }, + { + "name": "decimalsOffset_", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "initialMaxWithdrawalSize", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "feeRecipient_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "lowerMaxWithdrawalSize", + "inputs": [ + { + "name": "newSize", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "lowerYieldGroupCap", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "absoluteCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "percentageCapBps", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "maxDeposit", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "maxMint", + "inputs": [ + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "maxRedeem", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "maxWithdraw", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "maxWithdrawalSize", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "outerDepositQueue", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "outerWithdrawQueue", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pauseHub", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "pauseYieldGroup", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewDeposit", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewMint", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewRedeem", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewWithdraw", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "raiseMaxWithdrawalSize", + "inputs": [ + { + "name": "newSize", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "raiseYieldGroupCap", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "absoluteCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "percentageCapBps", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "reallocate", + "inputs": [ + { + "name": "withdraws", + "type": "tuple[]", + "internalType": "struct IHub.ReallocateLeg[]", + "components": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "deposits", + "type": "tuple[]", + "internalType": "struct IHub.ReallocateLeg[]", + "components": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "redeem", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "redeemFeeBps", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint16", + "internalType": "uint16" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "registeredYieldGroups", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeYieldGroup", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setAccessControlManager", + "inputs": [ + { + "name": "accessControlManager_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setFeeRecipient", + "inputs": [ + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setManagementFeeBps", + "inputs": [ + { + "name": "bps", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setOuterDepositQueue", + "inputs": [ + { + "name": "queue", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setOuterWithdrawQueue", + "inputs": [ + { + "name": "queue", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setPerformanceFeeBps", + "inputs": [ + { + "name": "bps", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setRedeemFeeBps", + "inputs": [ + { + "name": "bps", + "type": "uint16", + "internalType": "uint16" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "sweep", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalAssets", + "inputs": [], + "outputs": [ + { + "name": "total", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unpauseHub", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unpauseYieldGroup", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "yieldGroupConfig", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IHub.YieldGroupConfig", + "components": [ + { + "name": "absoluteCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "percentageCapBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "paused", + "type": "bool", + "internalType": "bool" + }, + { + "name": "registered", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "yieldGroupEffectiveCap", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Deposit", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assets", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "shares", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DepositRouted", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeeRecipientSet", + "inputs": [ + { + "name": "oldRecipient", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "newRecipient", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeesAccrued", + "inputs": [ + { + "name": "managementShares", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "performanceShares", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "totalShares", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HighWaterMarkUpdated", + "inputs": [ + { + "name": "oldHwm", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newHwm", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HubPauseToggled", + "inputs": [ + { + "name": "paused", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ManagementFeeBpsSet", + "inputs": [ + { + "name": "oldBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + }, + { + "name": "newBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MaxWithdrawalSizeLowered", + "inputs": [ + { + "name": "oldSize", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newSize", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MaxWithdrawalSizeRaised", + "inputs": [ + { + "name": "oldSize", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newSize", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "NewAccessControlManager", + "inputs": [ + { + "name": "oldAccessControlManager", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "newAccessControlManager", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OuterDepositQueueSet", + "inputs": [ + { + "name": "queue", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OuterWithdrawQueueSet", + "inputs": [ + { + "name": "queue", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PerformanceFeeBpsSet", + "inputs": [ + { + "name": "oldBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + }, + { + "name": "newBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RedeemFeeBpsSet", + "inputs": [ + { + "name": "oldBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + }, + { + "name": "newBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Swept", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Withdraw", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "receiver", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assets", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "shares", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "WithdrawRouted", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "YieldGroupAdded", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "absoluteCap", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "percentageCapBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "YieldGroupCapLowered", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "absoluteCap", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "percentageCapBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "YieldGroupCapRaised", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "absoluteCap", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "percentageCapBps", + "type": "uint16", + "indexed": false, + "internalType": "uint16" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "YieldGroupPauseToggled", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "paused", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "YieldGroupRemoved", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "YieldGroupSkipped", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "isDeposit", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "HubCapacityExceeded", + "inputs": [ + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAvailable", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "HubInsufficientLiquidity", + "inputs": [ + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "HubPaused", + "inputs": [] + }, + { + "type": "error", + "name": "HubWithdrawCapExceeded", + "inputs": [ + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "perRequestCap", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidCap", + "inputs": [ + { + "name": "absoluteCap", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "percentageCapBps", + "type": "uint16", + "internalType": "uint16" + } + ] + }, + { + "type": "error", + "name": "InvalidDecimalsOffset", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidFeeBps", + "inputs": [ + { + "name": "bps", + "type": "uint16", + "internalType": "uint16" + } + ] + }, + { + "type": "error", + "name": "InvalidQueue", + "inputs": [] + }, + { + "type": "error", + "name": "NotDecreasing", + "inputs": [] + }, + { + "type": "error", + "name": "NotIncreasing", + "inputs": [] + }, + { + "type": "error", + "name": "ReallocateImbalanced", + "inputs": [ + { + "name": "withdrawSum", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "depositSum", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "SweepProtectedAsset", + "inputs": [] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "calledContract", + "type": "address", + "internalType": "address" + }, + { + "name": "methodSignature", + "type": "string", + "internalType": "string" + } + ] + }, + { + "type": "error", + "name": "WithdrawQueueOmitsFundedYieldGroup", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "YieldGroupAlreadyRegistered", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "YieldGroupAssetMismatch", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "yieldGroupAsset", + "type": "address", + "internalType": "address" + }, + { + "name": "hubAsset", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "YieldGroupHasBalance", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "YieldGroupNotRegistered", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "YieldGroupPaused", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "YieldGroupUnderfilled", + "inputs": [ + { + "name": "yieldGroup", + "type": "address", + "internalType": "address" + }, + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroAmount", + "inputs": [] + } +] diff --git a/simulations/vip-680/abi/YieldGroup.json b/simulations/vip-680/abi/YieldGroup.json new file mode 100644 index 000000000..be5b44c73 --- /dev/null +++ b/simulations/vip-680/abi/YieldGroup.json @@ -0,0 +1,1077 @@ +[ + { + "type": "function", + "name": "accessControlManager", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "accrue", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addResource", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "adapter", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "asset", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "blocksPerYear", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "deposited", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "depositResource", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "deposited", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "dispatchDepositExternal", + "inputs": [ + { + "name": "adapter", + "type": "address", + "internalType": "address" + }, + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "placed", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "dispatchWithdrawExternal", + "inputs": [ + { + "name": "adapter", + "type": "address", + "internalType": "address" + }, + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "received", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "hub", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "hub_", + "type": "address", + "internalType": "address" + }, + { + "name": "asset_", + "type": "address", + "internalType": "address" + }, + { + "name": "blocksPerYear_", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "acm_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "innerDepositQueue", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "innerWithdrawQueue", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "lowerResourceCap", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "newCap", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "maxDeposit", + "inputs": [], + "outputs": [ + { + "name": "total", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "maxWithdraw", + "inputs": [], + "outputs": [ + { + "name": "total", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pauseResource", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "raiseResourceCap", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "newCap", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeResource", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "resourceCap", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "resourceConfig", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "registered", + "type": "bool", + "internalType": "bool" + }, + { + "name": "paused", + "type": "bool", + "internalType": "bool" + }, + { + "name": "adapter", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "resources", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setBlocksPerYear", + "inputs": [ + { + "name": "newBlocksPerYear", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setInnerDepositQueue", + "inputs": [ + { + "name": "queue", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setInnerWithdrawQueue", + "inputs": [ + { + "name": "queue", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "spotAPYBps", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "sweep", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "totalAssets", + "inputs": [], + "outputs": [ + { + "name": "total", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "unpauseResource", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateResourceAdapter", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "newAdapter", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdrawResource", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "BlocksPerYearSet", + "inputs": [ + { + "name": "oldBlocksPerYear", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newBlocksPerYear", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DepositRouted", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "InnerDepositQueueSet", + "inputs": [ + { + "name": "queue", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "InnerWithdrawQueueSet", + "inputs": [ + { + "name": "queue", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ResourceAccrualFailed", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "adapter", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ResourceAdapterUpdated", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "oldAdapter", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newAdapter", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ResourceAdded", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "adapter", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ResourceCapLowered", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newCap", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ResourceCapRaised", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newCap", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ResourcePauseToggled", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "paused", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ResourceRemoved", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ResourceSkipped", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "isDeposit", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Swept", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "WithdrawRouted", + "inputs": [ + { + "name": "resource", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AdapterNotContract", + "inputs": [ + { + "name": "adapter", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AdapterUnderfilled", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "HubAssetMismatch", + "inputs": [ + { + "name": "declared", + "type": "address", + "internalType": "address" + }, + { + "name": "hubAsset", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "InvalidQueue", + "inputs": [] + }, + { + "type": "error", + "name": "NotDecreasing", + "inputs": [] + }, + { + "type": "error", + "name": "NotHub", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "NotIncreasing", + "inputs": [] + }, + { + "type": "error", + "name": "OnlySelf", + "inputs": [] + }, + { + "type": "error", + "name": "ResourceAlreadyRegistered", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ResourceAssetMismatch", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "resourceAsset", + "type": "address", + "internalType": "address" + }, + { + "name": "expected", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ResourceBelowMinimumDeposit", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimum", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ResourceCapacityExceeded", + "inputs": [ + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAvailable", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ResourceHasBalance", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + }, + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ResourceIsPaused", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ResourceLiquidityInsufficient", + "inputs": [ + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ResourceNotContract", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ResourceNotRegistered", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "SweepProtectedAsset", + "inputs": [] + }, + { + "type": "error", + "name": "SweepProtectedResource", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + }, + { + "name": "signature", + "type": "string", + "internalType": "string" + } + ] + }, + { + "type": "error", + "name": "WithdrawQueueOmitsFundedResource", + "inputs": [ + { + "name": "resource", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [] + } +] diff --git a/simulations/vip-680/bsctestnet-addendum.ts b/simulations/vip-680/bsctestnet-addendum.ts new file mode 100644 index 000000000..bf9c117ef --- /dev/null +++ b/simulations/vip-680/bsctestnet-addendum.ts @@ -0,0 +1,95 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { expectEvents } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import { + CORE_SOURCE_GOVERNANCE_SIGS, + CORE_SOURCE_USDT, + HUB_GOVERNANCE_SIGS, + HUB_USDT, +} from "../../vips/vip-680/bsctestnet"; +import vip680Addendum from "../../vips/vip-680/bsctestnet-addendum"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; + +const { FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, ACCESS_CONTROL_MANAGER } = NETWORK_ADDRESSES.bsctestnet; + +const FAST_LANE_TIMELOCKS = [FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; + +// bsctestnet block after the Liquidity Hub (USDT) deployment (deploy block ~117918419). +const BLOCK_NUMBER = 117930000; + +const roleId = (contract: string, sig: string) => + ethers.utils.solidityKeccak256(["address", "string"], [contract, sig]); + +// reallocate is the Operator-only role — it is NOT part of the Governance set this addendum grants, so +// the fast-lane timelocks must never receive it. +const REALLOCATE_SIG = "reallocate((address,address,uint256)[],(address,address,uint256)[])"; + +forking(BLOCK_NUMBER, async () => { + let acm: Contract; + + before(async () => { + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACCESS_CONTROL_MANAGER); + }); + + describe("Pre-VIP state", () => { + it("Fast-Track and Critical timelocks hold no governance role on Hub or Core source", async () => { + for (const timelock of FAST_LANE_TIMELOCKS) { + for (const sig of HUB_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), timelock)).to.equal( + false, + `pre Hub gov ${sig} @ ${timelock}`, + ); + } + for (const sig of CORE_SOURCE_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), timelock)).to.equal( + false, + `pre Core gov ${sig} @ ${timelock}`, + ); + } + } + }); + }); + + testVip( + "VIP-680 [BNB Testnet] Liquidity Hub (USDT) Fast-Track/Critical permissions (addendum)", + await vip680Addendum(), + { + callbackAfterExecution: async txResponse => { + // Governance set (18 Hub + 11 Core = 29) granted to 2 timelocks = 58 RoleGranted events. + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [58]); + }, + }, + ); + + describe("Post-VIP permissions", () => { + it("Fast-Track and Critical timelocks hold the full governance role set on Hub and Core source", async () => { + for (const timelock of FAST_LANE_TIMELOCKS) { + for (const sig of HUB_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), timelock)).to.equal( + true, + `post Hub gov ${sig} @ ${timelock}`, + ); + } + for (const sig of CORE_SOURCE_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), timelock)).to.equal( + true, + `post Core gov ${sig} @ ${timelock}`, + ); + } + } + }); + + it("Fast-Track and Critical timelocks do NOT hold the operator-only reallocate role", async () => { + for (const timelock of FAST_LANE_TIMELOCKS) { + expect(await acm.hasRole(roleId(HUB_USDT, REALLOCATE_SIG), timelock)).to.equal( + false, + `reallocate @ ${timelock}`, + ); + } + }); + }); +}); diff --git a/simulations/vip-680/bsctestnet.ts b/simulations/vip-680/bsctestnet.ts new file mode 100644 index 000000000..8c5a35b3c --- /dev/null +++ b/simulations/vip-680/bsctestnet.ts @@ -0,0 +1,201 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { expectEvents } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import vip680, { + ADAPTER_CORE_V1, + CORE_ABSOLUTE_CAP, + CORE_SOURCE_GOVERNANCE_SIGS, + CORE_SOURCE_OPERATOR_SIGS, + CORE_SOURCE_USDT, + HUB_GOVERNANCE_SIGS, + HUB_OPERATOR_SIGS, + HUB_USDT, + PERCENTAGE_CAP_DISABLED, + VUSDT_CORE, +} from "../../vips/vip-680/bsctestnet"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; +import HUB_ABI from "./abi/Hub.json"; +import YIELD_GROUP_ABI from "./abi/YieldGroup.json"; + +const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN, ACCESS_CONTROL_MANAGER } = + NETWORK_ADDRESSES.bsctestnet; + +// The Fast-Track / Critical timelocks receive their governance grants in the addendum proposal, so +// they must stay ungranted here — this proves the split boundary. +const FAST_LANE_TIMELOCKS = [FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; + +// bsctestnet block after the Liquidity Hub (USDT) deployment (deploy block ~117918419). +const BLOCK_NUMBER = 117930000; + +// The exact ACM role id `giveCallPermission(contract, sig, account)` grants: +// keccak256(abi.encodePacked(contract, functionSig)). Asserting `hasRole` on this specific role is +// immune to the `isAllowedToCall` wildcard fallback (a role granted against address(0)), so pre-VIP +// "not granted" checks cannot be masked by an unrelated wildcard permission. +const roleId = (contract: string, sig: string) => + ethers.utils.solidityKeccak256(["address", "string"], [contract, sig]); + +const addr = (a: string) => ethers.utils.getAddress(a); + +// Governance-only signatures = governance set minus the shared (also-Operator) tighten actions. +const HUB_GOVERNANCE_ONLY_SIGS = HUB_GOVERNANCE_SIGS.filter(s => !HUB_OPERATOR_SIGS.includes(s)); +const CORE_SOURCE_GOVERNANCE_ONLY_SIGS = CORE_SOURCE_GOVERNANCE_SIGS.filter( + s => !CORE_SOURCE_OPERATOR_SIGS.includes(s), +); + +const REALLOCATE_SIG = "reallocate((address,address,uint256)[],(address,address,uint256)[])"; + +forking(BLOCK_NUMBER, async () => { + let acm: Contract; + let hub: Contract; + let coreSource: Contract; + + before(async () => { + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACCESS_CONTROL_MANAGER); + hub = await ethers.getContractAt(HUB_ABI, HUB_USDT); + coreSource = await ethers.getContractAt(YIELD_GROUP_ABI, CORE_SOURCE_USDT); + }); + + describe("Pre-VIP state", () => { + it("Hub has no registered yield groups and empty outer queues", async () => { + expect(await hub.registeredYieldGroups()).to.deep.equal([]); + expect(await hub.outerDepositQueue()).to.deep.equal([]); + expect(await hub.outerWithdrawQueue()).to.deep.equal([]); + }); + + it("Core source has no registered resources and empty inner queues", async () => { + expect(await coreSource.resources()).to.deep.equal([]); + expect(await coreSource.innerDepositQueue()).to.deep.equal([]); + expect(await coreSource.innerWithdrawQueue()).to.deep.equal([]); + }); + + it("Hub advertises zero deposit capacity (no routes wired)", async () => { + expect(await hub.maxDeposit(NORMAL_TIMELOCK)).to.equal(0); + }); + + it("Normal timelock holds no governance role on Hub or Core source", async () => { + for (const sig of HUB_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), NORMAL_TIMELOCK)).to.equal(false, `pre Hub gov ${sig}`); + } + for (const sig of CORE_SOURCE_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), NORMAL_TIMELOCK)).to.equal( + false, + `pre Core gov ${sig}`, + ); + } + }); + + it("Guardian holds no operator role on Hub or Core source", async () => { + for (const sig of HUB_OPERATOR_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), GUARDIAN)).to.equal(false, `pre Hub op ${sig}`); + } + for (const sig of CORE_SOURCE_OPERATOR_SIGS) { + expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), GUARDIAN)).to.equal(false, `pre Core op ${sig}`); + } + }); + }); + + testVip("VIP-680 [BNB Testnet] Configure Liquidity Hub (USDT)", await vip680(), { + callbackAfterExecution: async txResponse => { + // Normal-timelock governance: 18 Hub + 11 Core = 29. Guardian operator: 7 Hub + 4 Core = 11. Total 40. + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [40]); + await expectEvents( + txResponse, + [HUB_ABI], + ["YieldGroupAdded", "OuterDepositQueueSet", "OuterWithdrawQueueSet"], + [1, 1, 1], + ); + await expectEvents( + txResponse, + [YIELD_GROUP_ABI], + ["ResourceAdded", "InnerDepositQueueSet", "InnerWithdrawQueueSet"], + [1, 1, 1], + ); + }, + }); + + describe("Post-VIP wiring", () => { + it("Core source registered on the Hub with the expected caps", async () => { + expect((await hub.registeredYieldGroups()).map(addr)).to.deep.equal([addr(CORE_SOURCE_USDT)]); + const cfg = await hub.yieldGroupConfig(CORE_SOURCE_USDT); + expect(cfg.registered).to.equal(true); + expect(cfg.paused).to.equal(false); + expect(cfg.absoluteCap.toString()).to.equal(CORE_ABSOLUTE_CAP); + expect(cfg.percentageCapBps).to.equal(PERCENTAGE_CAP_DISABLED); + }); + + it("Hub outer queues route to the Core source", async () => { + expect((await hub.outerDepositQueue()).map(addr)).to.deep.equal([addr(CORE_SOURCE_USDT)]); + expect((await hub.outerWithdrawQueue()).map(addr)).to.deep.equal([addr(CORE_SOURCE_USDT)]); + }); + + it("vUSDT registered on the Core source behind AdapterCoreV1", async () => { + expect((await coreSource.resources()).map(addr)).to.deep.equal([addr(VUSDT_CORE)]); + const [registered, paused, adapter] = await coreSource.resourceConfig(VUSDT_CORE); + expect(registered).to.equal(true); + expect(paused).to.equal(false); + expect(addr(adapter)).to.equal(addr(ADAPTER_CORE_V1)); + }); + + it("Core source inner queues route to vUSDT", async () => { + expect((await coreSource.innerDepositQueue()).map(addr)).to.deep.equal([addr(VUSDT_CORE)]); + expect((await coreSource.innerWithdrawQueue()).map(addr)).to.deep.equal([addr(VUSDT_CORE)]); + }); + + it("Hub now advertises deposit capacity through Core → vUSDT", async () => { + // Non-zero maxDeposit exercises the whole wired route: Hub outer queue → Core source inner + // queue → AdapterCoreV1.maxDeposit(vUSDT) (mint unpaused, supply-cap headroom). + expect(await hub.maxDeposit(NORMAL_TIMELOCK)).to.be.gt(0); + }); + }); + + describe("Post-VIP permissions", () => { + it("Normal timelock holds the full governance role set on Hub and Core source", async () => { + for (const sig of HUB_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), NORMAL_TIMELOCK)).to.equal(true, `post Hub gov ${sig}`); + } + for (const sig of CORE_SOURCE_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), NORMAL_TIMELOCK)).to.equal( + true, + `post Core gov ${sig}`, + ); + } + }); + + it("Guardian holds the full operator role set on Hub and Core source", async () => { + for (const sig of HUB_OPERATOR_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), GUARDIAN)).to.equal(true, `post Hub op ${sig}`); + } + for (const sig of CORE_SOURCE_OPERATOR_SIGS) { + expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), GUARDIAN)).to.equal(true, `post Core op ${sig}`); + } + }); + + it("Guardian holds NO governance-only role (asymmetric: Operator can only tighten)", async () => { + for (const sig of HUB_GOVERNANCE_ONLY_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), GUARDIAN)).to.equal(false, `Guardian must NOT have Hub ${sig}`); + } + for (const sig of CORE_SOURCE_GOVERNANCE_ONLY_SIGS) { + expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), GUARDIAN)).to.equal( + false, + `Guardian must NOT have Core ${sig}`, + ); + } + }); + + it("Normal timelock does NOT hold the operator-only reallocate role", async () => { + expect(await acm.hasRole(roleId(HUB_USDT, REALLOCATE_SIG), NORMAL_TIMELOCK)).to.equal(false); + }); + + it("Fast-Track and Critical timelocks are not yet granted (deferred to the addendum)", async () => { + for (const timelock of FAST_LANE_TIMELOCKS) { + for (const sig of HUB_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), timelock)).to.equal(false, `${sig} @ ${timelock}`); + } + } + }); + }); +}); diff --git a/vips/vip-680/bsctestnet-addendum.ts b/vips/vip-680/bsctestnet-addendum.ts new file mode 100644 index 000000000..cd04215c6 --- /dev/null +++ b/vips/vip-680/bsctestnet-addendum.ts @@ -0,0 +1,65 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { + CORE_SOURCE_GOVERNANCE_SIGS, + CORE_SOURCE_USDT, + HUB_GOVERNANCE_SIGS, + HUB_USDT, + giveCallPermission, +} from "./bsctestnet"; + +const { FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK } = NETWORK_ADDRESSES.bsctestnet; + +// Addendum to VIP-680. The main proposal grants the Liquidity Hub (USDT) Governance role set to the +// Normal timelock (plus the Operator set to the Guardian) and performs the Core wiring. This addendum +// grants the SAME Governance role set on Hub_USDT and CoreSource_USDT to the Fast-Track and Critical +// timelocks, so urgent proposals can operate the Hub too. It is a separate proposal because all three +// timelocks' governance grants plus the wiring exceed a single proposal's block-gas budget on BSC. +// Addresses, ACM target, and role strings are imported from the main VIP-680 file as the single +// source of truth. +const FAST_LANE_TIMELOCKS = [FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; + +export const vip680Addendum = () => { + const meta = { + version: "v2", + title: "VIP-680 [BNB Chain Testnet] Liquidity Hub (USDT) — Fast-Track and Critical timelock permissions (addendum)", + description: `#### Summary + +Addendum to VIP-680. Grants the **Governance** role set on the **Liquidity Hub (USDT)** and its +**Core yield source** to the **Fast-Track** and **Critical** timelocks on BNB Chain Testnet, so +urgent (fast-track / critical) proposals can operate the Hub in addition to the Normal timelock +configured in the main VIP-680 proposal. + +#### Actions + +Grant, on both **Hub_USDT** and the **Core source**, the full Governance role set (registry, caps, +queues, pause/unpause, fees, sweep, adapter updates, \`emergencyReallocate\`) to the Fast-Track and +Critical timelocks. + +#### Notes + +- Governance-only: this proposal grants no Operator (\`reallocate\` / tighten-only) roles — those are + held by the Guardian per the main VIP-680 proposal. +- Testnet-only. The Hub is not yet deployed on any mainnet.`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + ...FAST_LANE_TIMELOCKS.flatMap(timelock => + HUB_GOVERNANCE_SIGS.map(sig => giveCallPermission(HUB_USDT, sig, timelock)), + ), + ...FAST_LANE_TIMELOCKS.flatMap(timelock => + CORE_SOURCE_GOVERNANCE_SIGS.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, timelock)), + ), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip680Addendum; diff --git a/vips/vip-680/bsctestnet.ts b/vips/vip-680/bsctestnet.ts new file mode 100644 index 000000000..5ba9ce279 --- /dev/null +++ b/vips/vip-680/bsctestnet.ts @@ -0,0 +1,205 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { NORMAL_TIMELOCK, GUARDIAN, ACCESS_CONTROL_MANAGER } = NETWORK_ADDRESSES.bsctestnet; + +// AccessControlManager that the Liquidity Hub contracts were initialised with. Verified on-chain to +// equal Hub_USDT.accessControlManager() and CoreSource_USDT.accessControlManager(), and to match the +// governance-contracts testnet ACM (NETWORK_ADDRESSES.bsctestnet.ACCESS_CONTROL_MANAGER = +// 0x45f8a08F534f34A97187626E05d4b6648Eeaa9AA). Grants must target THIS ACM — it is the one the Hub +// and sources check against in _checkAccessAllowed. +export const ACM = ACCESS_CONTROL_MANAGER; + +// --------------------------------------------------------------------------------------------------- +// Liquidity Hub (USDT) — hardcoded from venus-liquidity-hub/deployments/bsctestnet/*.json. +// The Hub is not published as an @venusprotocol/*-deployments npm package, so addresses are inlined +// here rather than imported. Cross-checked against the deployment registry and on-chain state. +// --------------------------------------------------------------------------------------------------- + +// Per-asset ERC-4626 Hub (USDT) +export const HUB_USDT = "0x8C8894217b9552736CF86784B087b5114b7CfF76"; + +// Core yield source (a YieldGroup proxy over Venus Core lending) — the one source wired by this VIP. +export const CORE_SOURCE_USDT = "0x5A53efCa9ac93c6456d60E3c33839e3F06BA9356"; + +// Deferred yield sources — deployed but intentionally NOT wired by this VIP (no concrete resource on +// testnet). See the file-level note below; a follow-up VIP wires each when its resource exists. +export const FRV_SOURCE_USDT = "0x503BF2929232a0Cf7C1D296a8C59D63C6224777D"; +export const FLUX_SOURCE_USDT = "0x8Aac02EB8054F4CBAdE1396651b94F8F7D87fafc"; + +// Core resource + adapter: Venus Core-pool vUSDT and the shared stateless AdapterCoreV1. +// vUSDT is @venusprotocol/venus-protocol bsctestnet `vUSDT`; verified on-chain that +// vUSDT.underlying() == Hub_USDT.asset() (USDT) and its Comptroller.treasuryPercent() == 0 +// (required for AdapterCoreV1.validateRegistration to pass). +export const VUSDT_CORE = "0xb7526572FFE56AB9D7489838Bf2E18e3323b441A"; +export const ADAPTER_CORE_V1 = "0x480B8A3BBb6B16920c3B60cDcD64Ad059EfDb352"; + +// --------------------------------------------------------------------------------------------------- +// Caps for addYieldGroup(source, absoluteCap, percentageCapBps). +// --------------------------------------------------------------------------------------------------- + +// Effectively-unbounded absolute cap. The Hub rejects type(uint256).max as InvalidCap, so +// type(uint128).max is the canonical "no ceiling" sentinel used throughout the Hub test-suite. +export const CORE_ABSOLUTE_CAP = "340282366920938463463374607431768211455"; // type(uint128).max +// 10_000 bps disables the percentage-of-TVL cap dimension, leaving only the absolute cap binding. +export const PERCENTAGE_CAP_DISABLED = 10_000; + +// --------------------------------------------------------------------------------------------------- +// ACM role strings — copied verbatim from the literals passed to _checkAccessAllowed(...) in the +// target contracts (Hub.sol / YieldGroupBase.sol / YieldGroup.sol). Split by access class per the +// Hub's asymmetric permission model (README "Permissions"): Governance can loosen AND tighten; the +// Operator can only tighten (lower caps, pause, reorder queues) plus reallocate. +// +// This VIP (VIP-680) provisions the Governance role set for the NORMAL timelock (the executor, which +// then performs the wiring) and the Operator role set for the Guardian. The Fast-Track and Critical +// timelocks receive the same Governance role set in the addendum proposal (bsctestnet-addendum.ts) — +// split out because all three timelocks' governance grants plus the wiring exceed the single-proposal +// block-gas budget on BSC. +// --------------------------------------------------------------------------------------------------- + +// Hub_USDT — Governance role set (every gated Hub function except `reallocate`, which is Operator-only). +export const HUB_GOVERNANCE_SIGS = [ + "addYieldGroup(address,uint256,uint16)", + "removeYieldGroup(address)", + "raiseYieldGroupCap(address,uint256,uint16)", + "lowerYieldGroupCap(address,uint256,uint16)", + "setOuterDepositQueue(address[])", + "setOuterWithdrawQueue(address[])", + "emergencyReallocate((address,address,uint256)[],(address,address,uint256)[])", + "pauseHub()", + "unpauseHub()", + "pauseYieldGroup(address)", + "unpauseYieldGroup(address)", + "raiseMaxWithdrawalSize(uint256)", + "lowerMaxWithdrawalSize(uint256)", + "setManagementFeeBps(uint16)", + "setPerformanceFeeBps(uint16)", + "setRedeemFeeBps(uint16)", + "setFeeRecipient(address)", + "sweep(address,address)", +]; + +// Hub_USDT — Operator role set (tighten-only + reallocate + emergency pause). +export const HUB_OPERATOR_SIGS = [ + "lowerYieldGroupCap(address,uint256,uint16)", + "setOuterDepositQueue(address[])", + "setOuterWithdrawQueue(address[])", + "reallocate((address,address,uint256)[],(address,address,uint256)[])", + "pauseHub()", + "pauseYieldGroup(address)", + "lowerMaxWithdrawalSize(uint256)", +]; + +// CoreSource_USDT — Governance role set (all gated YieldGroup functions). +export const CORE_SOURCE_GOVERNANCE_SIGS = [ + "addResource(address,address)", + "removeResource(address)", + "updateResourceAdapter(address,address)", + "setInnerDepositQueue(address[])", + "setInnerWithdrawQueue(address[])", + "pauseResource(address)", + "unpauseResource(address)", + "raiseResourceCap(address,uint256)", + "lowerResourceCap(address,uint256)", + "setBlocksPerYear(uint256)", + "sweep(address,address)", +]; + +// CoreSource_USDT — Operator role set (tighten-only). +export const CORE_SOURCE_OPERATOR_SIGS = [ + "setInnerDepositQueue(address[])", + "setInnerWithdrawQueue(address[])", + "pauseResource(address)", + "lowerResourceCap(address,uint256)", +]; + +// Operator holder: the testnet Guardian multisig (the Venus Core multisig plays the Operator role). +const OPERATOR = GUARDIAN; + +export const giveCallPermission = (contract: string, sig: string, account: string) => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [contract, sig, account], +}); + +export const vip680 = () => { + const meta = { + version: "v2", + title: "VIP-680 [BNB Chain Testnet] Configure Liquidity Hub (USDT) — permissions and Core yield source", + description: `#### Summary + +Post-deploy governance wiring for the newly deployed **Liquidity Hub (USDT)** on BNB Chain Testnet. +The Hub and its yield-source proxies were deployed without any ACM permissions, registry, or queue +configuration (the deploy scripts only deploy and initialise; all ACM-gated wiring is governance's +job). This VIP provisions the core access-control roles and wires the **Core** yield source +end-to-end so the USDT Hub becomes usable, routing deposits and withdrawals through Venus Core. A +companion addendum proposal grants the same Governance role set to the Fast-Track and Critical +timelocks. + +#### Access-control model + +The Hub uses an asymmetric permission model: **Governance** can both loosen and tighten, while the +**Operator** can only tighten (lower caps, pause, reorder queues) plus rebalance via \`reallocate\`. + +1. Grant the **Governance** role set on **Hub_USDT** and the **Core source** to the Normal Timelock + (registry, caps, queues, pause/unpause, fees, sweep, adapter updates, \`emergencyReallocate\`). +2. Grant the **Operator** role set on **Hub_USDT** and the **Core source** to the Guardian multisig + (\`lowerYieldGroupCap\`, \`lowerMaxWithdrawalSize\`, \`lowerResourceCap\`, \`setOuter*/setInner*Queue\`, + \`pauseHub\`, \`pauseYieldGroup\`, \`pauseResource\`, \`reallocate\`). + +#### Wiring + +3. Register **vUSDT** (Venus Core pool) on the Core source behind the shared **AdapterCoreV1**, and + point the source's inner deposit/withdraw queues at it. +4. Register the Core source on **Hub_USDT** with an effectively-unbounded cap, and point the Hub's + outer deposit/withdraw queues at it. + +#### Notes + +- **FRV and Flux sources are deferred.** Both proxies are deployed but have no concrete resource on + testnet: no FRV vault instance exists for USDT (only the vault implementation and controller are + deployed), and the Flux adapter is not deployed (no Fluid LendingResolver on testnet). Their + permissions and registration will be handled by a dedicated follow-up VIP once each resource exists. +- Testnet-only. The Hub is not yet deployed on any mainnet.`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + // 1. Governance role set (loosen + tighten) → the Normal Timelock (this proposal's executor). + ...HUB_GOVERNANCE_SIGS.map(sig => giveCallPermission(HUB_USDT, sig, NORMAL_TIMELOCK)), + ...CORE_SOURCE_GOVERNANCE_SIGS.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, NORMAL_TIMELOCK)), + + // 2. Operator role set (tighten-only + reallocate + emergency pause) → the Guardian multisig. + ...HUB_OPERATOR_SIGS.map(sig => giveCallPermission(HUB_USDT, sig, OPERATOR)), + ...CORE_SOURCE_OPERATOR_SIGS.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, OPERATOR)), + + // 3. Register vUSDT on the Core source behind AdapterCoreV1, then set its inner queues. + // addResource must precede the inner-queue setters (they reject unregistered resources). + { + target: CORE_SOURCE_USDT, + signature: "addResource(address,address)", + params: [VUSDT_CORE, ADAPTER_CORE_V1], + }, + { target: CORE_SOURCE_USDT, signature: "setInnerDepositQueue(address[])", params: [[VUSDT_CORE]] }, + { target: CORE_SOURCE_USDT, signature: "setInnerWithdrawQueue(address[])", params: [[VUSDT_CORE]] }, + + // 4. Register the Core source on the Hub, then point the outer queues at it. + // addYieldGroup must precede the outer-queue setters (they reject unregistered sources). + { + target: HUB_USDT, + signature: "addYieldGroup(address,uint256,uint16)", + params: [CORE_SOURCE_USDT, CORE_ABSOLUTE_CAP, PERCENTAGE_CAP_DISABLED], + }, + { target: HUB_USDT, signature: "setOuterDepositQueue(address[])", params: [[CORE_SOURCE_USDT]] }, + { target: HUB_USDT, signature: "setOuterWithdrawQueue(address[])", params: [[CORE_SOURCE_USDT]] }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip680; From b43df9ffefaaa0e47a6498e3d916f8d45012c585 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 9 Jul 2026 17:15:23 +0530 Subject: [PATCH 2/3] feat: grant testnet Guardian full Hub permissions - New proposal grants the Guardian the full Governance role set across the Hub stack (Hub, Core, FRV, Flux) so resources can be listed and reconfigured via multisig without a proposal per change - Deliberate testnet-only deviation from the asymmetric model where the Operator is tighten-only; kept in a bsctestnet file, not for mainnet - Factor the shared YieldGroupBase signatures into one base array; FRV uses YieldGroupFRV (adds forceRemoveResource, no cap setters or setBlocksPerYear) so it keeps a separate set - Update the main and addendum descriptions to reference the three proposals as one package --- simulations/vip-680/bsctestnet-guardian.ts | 114 ++++++++++++++++++++ vips/vip-680/bsctestnet-addendum.ts | 5 +- vips/vip-680/bsctestnet-guardian.ts | 117 +++++++++++++++++++++ vips/vip-680/bsctestnet.ts | 40 +++++-- 4 files changed, 264 insertions(+), 12 deletions(-) create mode 100644 simulations/vip-680/bsctestnet-guardian.ts create mode 100644 vips/vip-680/bsctestnet-guardian.ts diff --git a/simulations/vip-680/bsctestnet-guardian.ts b/simulations/vip-680/bsctestnet-guardian.ts new file mode 100644 index 000000000..52765daae --- /dev/null +++ b/simulations/vip-680/bsctestnet-guardian.ts @@ -0,0 +1,114 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { expectEvents } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import { + CORE_SOURCE_OPERATOR_SIGS, + CORE_SOURCE_USDT, + FLUX_SOURCE_USDT, + FRV_SOURCE_GOVERNANCE_SIGS, + FRV_SOURCE_USDT, + HUB_OPERATOR_SIGS, + HUB_USDT, +} from "../../vips/vip-680/bsctestnet"; +import vip680Guardian, { + CORE_SOURCE_GOVERNANCE_ONLY_SIGS, + FLUX_SOURCE_GOVERNANCE_SIGS, + HUB_GOVERNANCE_ONLY_SIGS, +} from "../../vips/vip-680/bsctestnet-guardian"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; + +const { GUARDIAN, ACCESS_CONTROL_MANAGER } = NETWORK_ADDRESSES.bsctestnet; + +// bsctestnet block after the Liquidity Hub (USDT) deployment (deploy block ~117918419). +const BLOCK_NUMBER = 117930000; + +// The exact ACM role id `giveCallPermission(contract, sig, account)` grants: +// keccak256(abi.encodePacked(contract, functionSig)). Asserting `hasRole` on this specific role is +// immune to the `isAllowedToCall` wildcard fallback (a role granted against address(0)), so pre-VIP +// "not granted" checks cannot be masked by an unrelated wildcard permission. +const roleId = (contract: string, sig: string) => + ethers.utils.solidityKeccak256(["address", "string"], [contract, sig]); + +// Every (contract, sig) pair this proposal grants to the Guardian. 12 + 7 + 9 + 11 = 39. +const GUARDIAN_GRANTS: [string, string][] = [ + ...HUB_GOVERNANCE_ONLY_SIGS.map((sig): [string, string] => [HUB_USDT, sig]), + ...CORE_SOURCE_GOVERNANCE_ONLY_SIGS.map((sig): [string, string] => [CORE_SOURCE_USDT, sig]), + ...FRV_SOURCE_GOVERNANCE_SIGS.map((sig): [string, string] => [FRV_SOURCE_USDT, sig]), + ...FLUX_SOURCE_GOVERNANCE_SIGS.map((sig): [string, string] => [FLUX_SOURCE_USDT, sig]), +]; + +forking(BLOCK_NUMBER, async () => { + let acm: Contract; + + before(async () => { + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACCESS_CONTROL_MANAGER); + }); + + describe("Pre-VIP state", () => { + it("this proposal grants exactly 39 (contract, sig) pairs to the Guardian", () => { + // 12 Hub gov-only + 7 Core gov-only + 9 FRV + 11 Flux. + expect(GUARDIAN_GRANTS.length).to.equal(39); + }); + + it("Guardian holds none of the governance roles this proposal grants", async () => { + for (const [contract, sig] of GUARDIAN_GRANTS) { + expect(await acm.hasRole(roleId(contract, sig), GUARDIAN)).to.equal(false, `pre ${contract} ${sig}`); + } + }); + + it("Guardian holds no role on the FRV or Flux sources yet", async () => { + for (const sig of FRV_SOURCE_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(FRV_SOURCE_USDT, sig), GUARDIAN)).to.equal(false, `pre FRV ${sig}`); + } + for (const sig of FLUX_SOURCE_GOVERNANCE_SIGS) { + expect(await acm.hasRole(roleId(FLUX_SOURCE_USDT, sig), GUARDIAN)).to.equal(false, `pre Flux ${sig}`); + } + }); + }); + + testVip("VIP-680 [BNB Testnet] Liquidity Hub (USDT) Guardian full permissions", await vip680Guardian(), { + callbackAfterExecution: async txResponse => { + // 12 Hub gov-only + 7 Core gov-only + 9 FRV + 11 Flux = 39 RoleGranted events. + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [39]); + }, + }); + + describe("Post-VIP permissions", () => { + it("Guardian now holds every governance role this proposal grants", async () => { + for (const [contract, sig] of GUARDIAN_GRANTS) { + expect(await acm.hasRole(roleId(contract, sig), GUARDIAN)).to.equal(true, `post ${contract} ${sig}`); + } + }); + + it("Guardian can now add resources / yield groups (loosening) on every target", async () => { + // The whole point of this testnet proposal: the Guardian holds the route-creating (governance) + // roles the mainnet asymmetric model reserves for the timelocks. + expect(await acm.hasRole(roleId(HUB_USDT, "addYieldGroup(address,uint256,uint16)"), GUARDIAN)).to.equal(true); + for (const source of [CORE_SOURCE_USDT, FRV_SOURCE_USDT, FLUX_SOURCE_USDT]) { + expect(await acm.hasRole(roleId(source, "addResource(address,address)"), GUARDIAN)).to.equal( + true, + `addResource @ ${source}`, + ); + } + }); + + it("does not grant the operator-only Hub roles (those come from the main VIP-680 proposal)", async () => { + // This file grants only the Governance(-only) signatures. The Operator/tighten set — including + // the operator-only `reallocate` and the shared queue/pause signatures — is granted to the + // Guardian by the main VIP-680 proposal, so on this standalone fork it stays ungranted here. + for (const sig of HUB_OPERATOR_SIGS) { + expect(await acm.hasRole(roleId(HUB_USDT, sig), GUARDIAN)).to.equal(false, `Hub op not granted here: ${sig}`); + } + for (const sig of CORE_SOURCE_OPERATOR_SIGS) { + expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), GUARDIAN)).to.equal( + false, + `Core op not granted here: ${sig}`, + ); + } + }); + }); +}); diff --git a/vips/vip-680/bsctestnet-addendum.ts b/vips/vip-680/bsctestnet-addendum.ts index cd04215c6..b232e4bf6 100644 --- a/vips/vip-680/bsctestnet-addendum.ts +++ b/vips/vip-680/bsctestnet-addendum.ts @@ -40,8 +40,9 @@ Critical timelocks. #### Notes -- Governance-only: this proposal grants no Operator (\`reallocate\` / tighten-only) roles — those are - held by the Guardian per the main VIP-680 proposal. +- This proposal grants timelock roles only. The Guardian's permissions are set separately: the + Operator (tighten-only) set by the main VIP-680 proposal, and the full Governance set by the + companion Guardian-permissions proposal (testnet-only). - Testnet-only. The Hub is not yet deployed on any mainnet.`, forDescription: "Execute this proposal", againstDescription: "Do not execute this proposal", diff --git a/vips/vip-680/bsctestnet-guardian.ts b/vips/vip-680/bsctestnet-guardian.ts new file mode 100644 index 000000000..dc6854f64 --- /dev/null +++ b/vips/vip-680/bsctestnet-guardian.ts @@ -0,0 +1,117 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { + CORE_SOURCE_GOVERNANCE_SIGS, + CORE_SOURCE_OPERATOR_SIGS, + CORE_SOURCE_USDT, + FLUX_SOURCE_USDT, + FRV_SOURCE_GOVERNANCE_SIGS, + FRV_SOURCE_USDT, + HUB_GOVERNANCE_SIGS, + HUB_OPERATOR_SIGS, + HUB_USDT, + giveCallPermission, +} from "./bsctestnet"; + +const { GUARDIAN } = NETWORK_ADDRESSES.bsctestnet; + +// --------------------------------------------------------------------------------------------------- +// VIP-680 (Guardian permissions) — TESTNET ONLY. +// +// The main VIP-680 proposal already grants the Guardian the Operator role set on Hub_USDT and the +// Core source. This proposal tops the Guardian up to the FULL Governance role set across the entire +// Hub stack (Hub_USDT + Core + FRV + Flux sources) so the backend can list, remove and reconfigure +// resources through the Guardian multisig during testing WITHOUT a governance proposal per change. +// +// This is a DELIBERATE testnet-only deviation from the mainnet asymmetric permission model, where the +// Operator (Guardian) is tighten-only and only Governance (the timelocks) may loosen — add routes, +// raise caps, unpause, update adapters, sweep. Do NOT mirror this proposal to mainnet. +// +// Because the Guardian already holds the Operator (tighten) signatures from the main proposal, this +// file only grants the Governance-only DELTAS on Hub_USDT and the Core source, plus the FULL +// Governance set on the FRV and Flux sources (where the Guardian holds nothing yet). Addresses, the +// ACM target and every role string are imported from the main VIP-680 file as the single source of +// truth. Flux reuses the Core role strings (identical YieldGroup code); FRV has its own set +// (YieldGroupFRV: no raise/lowerResourceCap, no setBlocksPerYear, plus forceRemoveResource). +// --------------------------------------------------------------------------------------------------- + +// Governance-only signatures = Governance set minus the shared (also-Operator) tighten actions the +// Guardian already holds from the main proposal. +export const HUB_GOVERNANCE_ONLY_SIGS = HUB_GOVERNANCE_SIGS.filter(sig => !HUB_OPERATOR_SIGS.includes(sig)); +export const CORE_SOURCE_GOVERNANCE_ONLY_SIGS = CORE_SOURCE_GOVERNANCE_SIGS.filter( + sig => !CORE_SOURCE_OPERATOR_SIGS.includes(sig), +); + +// Flux uses the same YieldGroup contract as Core, so its Governance role set is identical. The +// Guardian holds nothing on the Flux source yet, so it receives the full set here. +export const FLUX_SOURCE_GOVERNANCE_SIGS = CORE_SOURCE_GOVERNANCE_SIGS; + +export const vip680Guardian = () => { + const meta = { + version: "v2", + title: "VIP-680 [BNB Chain Testnet] Configure Liquidity Hub (USDT) — Guardian full permissions (testnet only)", + description: `#### Summary + +**Testnet-only.** Tops the Guardian multisig up to the **full Governance role set** across the entire +Liquidity Hub (USDT) stack on BNB Chain Testnet: **Hub_USDT**, the **Core**, **FRV** and **Flux** +yield sources. This lets the backend add, remove and reconfigure resources (list new FRV vaults / Flux +markets, change caps, pause/unpause, swap adapters, reorder queues) directly through the Guardian +multisig during testing, without a governance proposal per change. + +The main VIP-680 proposal already grants the Guardian the Operator (tighten-only) role set on +Hub_USDT and the Core source. This proposal grants the remaining Governance-only signatures on those +two contracts, plus the full Governance role set on the FRV and Flux sources (where the Guardian holds +nothing yet). + +#### Access-control note + +On mainnet the Hub uses an asymmetric model: only Governance (the timelocks) may loosen — add routes, +raise caps, unpause, update adapters, sweep — while the Operator (Guardian) is tighten-only. Granting +the Guardian the Governance set here is a **deliberate deviation made purely to speed up testnet +iteration, and must not be mirrored to mainnet.** + +#### Actions + +Grant to the **Guardian** multisig: + +1. The Governance-only signatures on **Hub_USDT** (registry, cap raises, unpause, fees, sweep, + \`emergencyReallocate\`) — completing the full Hub Governance set alongside its existing Operator + grants. +2. The Governance-only signatures on the **Core source** (\`addResource\`, \`removeResource\`, + \`updateResourceAdapter\`, \`unpauseResource\`, \`raiseResourceCap\`, \`setBlocksPerYear\`, \`sweep\`). +3. The full Governance role set on the **FRV source** (\`YieldGroupFRV\`). +4. The full Governance role set on the **Flux source** (\`YieldGroup\`, same as Core). + +#### Notes + +- Permissions only — this proposal performs no wiring. FRV and Flux still cannot be registered until a + real FRV vault instance / Flux adapter exists on testnet; the Guardian will do that via multisig + once each resource is available. +- Testnet-only. The Hub is not yet deployed on any mainnet.`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + // 1. Hub_USDT — Governance-only delta → Guardian (already holds the Operator/tighten set). + ...HUB_GOVERNANCE_ONLY_SIGS.map(sig => giveCallPermission(HUB_USDT, sig, GUARDIAN)), + + // 2. Core source — Governance-only delta → Guardian. + ...CORE_SOURCE_GOVERNANCE_ONLY_SIGS.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, GUARDIAN)), + + // 3. FRV source — full Governance set → Guardian (YieldGroupFRV role strings). + ...FRV_SOURCE_GOVERNANCE_SIGS.map(sig => giveCallPermission(FRV_SOURCE_USDT, sig, GUARDIAN)), + + // 4. Flux source — full Governance set → Guardian (same YieldGroup role strings as Core). + ...FLUX_SOURCE_GOVERNANCE_SIGS.map(sig => giveCallPermission(FLUX_SOURCE_USDT, sig, GUARDIAN)), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip680Guardian; diff --git a/vips/vip-680/bsctestnet.ts b/vips/vip-680/bsctestnet.ts index 5ba9ce279..8425d15a9 100644 --- a/vips/vip-680/bsctestnet.ts +++ b/vips/vip-680/bsctestnet.ts @@ -91,8 +91,14 @@ export const HUB_OPERATOR_SIGS = [ "lowerMaxWithdrawalSize(uint256)", ]; -// CoreSource_USDT — Governance role set (all gated YieldGroup functions). -export const CORE_SOURCE_GOVERNANCE_SIGS = [ +// Yield-source Governance role strings. All three sources share the YieldGroupBase gated surface; +// each subclass only adds a few functions (copied verbatim from YieldGroupBase.sol / YieldGroup.sol / +// YieldGroupFRV.sol): +// - Core & Flux use `YieldGroup` → base + raiseResourceCap / lowerResourceCap / setBlocksPerYear +// - FRV uses `YieldGroupFRV` → base + forceRemoveResource (no cap setters, no setBlocksPerYear) + +// Shared by all three sources (the 8 gated functions on YieldGroupBase). +export const YIELD_GROUP_BASE_GOVERNANCE_SIGS = [ "addResource(address,address)", "removeResource(address)", "updateResourceAdapter(address,address)", @@ -100,12 +106,21 @@ export const CORE_SOURCE_GOVERNANCE_SIGS = [ "setInnerWithdrawQueue(address[])", "pauseResource(address)", "unpauseResource(address)", + "sweep(address,address)", +]; + +// CoreSource_USDT (and the Flux source) — YieldGroup: base + per-resource caps + blocksPerYear. +export const CORE_SOURCE_GOVERNANCE_SIGS = [ + ...YIELD_GROUP_BASE_GOVERNANCE_SIGS, "raiseResourceCap(address,uint256)", "lowerResourceCap(address,uint256)", "setBlocksPerYear(uint256)", - "sweep(address,address)", ]; +// FRV_SOURCE_USDT — YieldGroupFRV: base + forceRemoveResource. It has NO raise/lowerResourceCap and +// NO setBlocksPerYear (those are Core/Flux-only), so it must not reuse CORE_SOURCE_GOVERNANCE_SIGS. +export const FRV_SOURCE_GOVERNANCE_SIGS = [...YIELD_GROUP_BASE_GOVERNANCE_SIGS, "forceRemoveResource(address)"]; + // CoreSource_USDT — Operator role set (tighten-only). export const CORE_SOURCE_OPERATOR_SIGS = [ "setInnerDepositQueue(address[])", @@ -133,9 +148,12 @@ Post-deploy governance wiring for the newly deployed **Liquidity Hub (USDT)** on The Hub and its yield-source proxies were deployed without any ACM permissions, registry, or queue configuration (the deploy scripts only deploy and initialise; all ACM-gated wiring is governance's job). This VIP provisions the core access-control roles and wires the **Core** yield source -end-to-end so the USDT Hub becomes usable, routing deposits and withdrawals through Venus Core. A -companion addendum proposal grants the same Governance role set to the Fast-Track and Critical -timelocks. +end-to-end so the USDT Hub becomes usable, routing deposits and withdrawals through Venus Core. + +Two companion proposals complete the testnet setup: an **addendum** grants the same Governance role +set to the Fast-Track and Critical timelocks, and a **Guardian-permissions** proposal tops the +Guardian up to the full Governance role set across the whole Hub stack (Hub + Core + FRV + Flux) so +backend can add and reconfigure resources via multisig without a VIP per change. #### Access-control model @@ -157,10 +175,12 @@ The Hub uses an asymmetric permission model: **Governance** can both loosen and #### Notes -- **FRV and Flux sources are deferred.** Both proxies are deployed but have no concrete resource on - testnet: no FRV vault instance exists for USDT (only the vault implementation and controller are - deployed), and the Flux adapter is not deployed (no Fluid LendingResolver on testnet). Their - permissions and registration will be handled by a dedicated follow-up VIP once each resource exists. +- **FRV and Flux sources are deferred in this proposal.** Both proxies are deployed but have no + concrete resource on testnet yet: no FRV vault instance exists for USDT (only the vault + implementation and controller are deployed), and the Flux adapter is not deployed (no Fluid + LendingResolver on testnet). Once each resource exists, the Guardian multisig registers it directly + using the permissions from the companion Guardian-permissions proposal — no further VIP needed on + testnet. - Testnet-only. The Hub is not yet deployed on any mainnet.`, forDescription: "Execute this proposal", againstDescription: "Do not execute this proposal", From 03cfb2d79db9153819c1773781806d02c9c05035 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 16 Jul 2026 20:02:45 +0530 Subject: [PATCH 3/3] feat(vip-680): onboard redeployed Liquidity Hub (USDT) The Liquidity Hub (USDT) stack was redeployed on BNB Chain testnet (new HubRegistry, Hub, Core/FRV/Flux sources and adapters), so the previous VIP-680 targets stale addresses and predates the registry. Rewrite it to onboard the current deployment on-chain in one package. - Split into five REGULAR proposals: the full surface (ownership accepts, ~205 role grants, source wiring) exceeds the BSC per-tx propose gas cap in a single proposal. - main: accept Hub and registry ownership, grant the Normal Timelock the governance set across the stack. - wiring: register the Hub, then wire Core, FRV and Flux end to end (addHub before addYieldGroup, outer queue [FRV, Flux, Core]). - fast-track and critical: grant each timelock the governance set. - guardian: grant the Guardian full permissions (testnet only). - move addresses and ACM role strings into addresses.ts and permissions.ts; add the HubRegistry ABI, regenerate the Hub and YieldGroup ABIs, and drop the superseded addendum files. --- simulations/vip-680/abi/Hub.json | 83 +++++ simulations/vip-680/abi/HubRegistry.json | 361 +++++++++++++++++++ simulations/vip-680/bsctestnet-addendum.ts | 95 ----- simulations/vip-680/bsctestnet-critical.ts | 74 ++++ simulations/vip-680/bsctestnet-fast-track.ts | 74 ++++ simulations/vip-680/bsctestnet-guardian.ts | 101 ++---- simulations/vip-680/bsctestnet-wiring.ts | 160 ++++++++ simulations/vip-680/bsctestnet.ts | 217 ++++------- vips/vip-680/addresses.ts | 75 ++++ vips/vip-680/bsctestnet-addendum.ts | 66 ---- vips/vip-680/bsctestnet-critical.ts | 71 ++++ vips/vip-680/bsctestnet-fast-track.ts | 74 ++++ vips/vip-680/bsctestnet-guardian.ts | 110 ++---- vips/vip-680/bsctestnet-wiring.ts | 121 +++++++ vips/vip-680/bsctestnet.ts | 247 +++---------- vips/vip-680/permissions.ts | 124 +++++++ 16 files changed, 1419 insertions(+), 634 deletions(-) create mode 100644 simulations/vip-680/abi/HubRegistry.json delete mode 100644 simulations/vip-680/bsctestnet-addendum.ts create mode 100644 simulations/vip-680/bsctestnet-critical.ts create mode 100644 simulations/vip-680/bsctestnet-fast-track.ts create mode 100644 simulations/vip-680/bsctestnet-wiring.ts create mode 100644 vips/vip-680/addresses.ts delete mode 100644 vips/vip-680/bsctestnet-addendum.ts create mode 100644 vips/vip-680/bsctestnet-critical.ts create mode 100644 vips/vip-680/bsctestnet-fast-track.ts create mode 100644 vips/vip-680/bsctestnet-wiring.ts create mode 100644 vips/vip-680/permissions.ts diff --git a/simulations/vip-680/abi/Hub.json b/simulations/vip-680/abi/Hub.json index 7867cd865..bd16f91fb 100644 --- a/simulations/vip-680/abi/Hub.json +++ b/simulations/vip-680/abi/Hub.json @@ -324,6 +324,35 @@ ], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "depositWithConsent", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "consentHash", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "emergencyReallocate", @@ -649,6 +678,35 @@ ], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "mintWithConsent", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "consentHash", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "name", @@ -1321,6 +1379,31 @@ ], "anonymous": false }, + { + "type": "event", + "name": "ConsentRecorded", + "inputs": [ + { + "name": "supplier", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "receiver", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "consentHash", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + } + ], + "anonymous": false + }, { "type": "event", "name": "Deposit", diff --git a/simulations/vip-680/abi/HubRegistry.json b/simulations/vip-680/abi/HubRegistry.json new file mode 100644 index 000000000..bd1afe17c --- /dev/null +++ b/simulations/vip-680/abi/HubRegistry.json @@ -0,0 +1,361 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "accessControlManager", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IAccessControlManagerV8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addHub", + "inputs": [ + { + "name": "hub", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getHubs", + "inputs": [], + "outputs": [ + { + "name": "hubs", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getHubsCount", + "inputs": [], + "outputs": [ + { + "name": "count", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "hubForAsset", + "inputs": [ + { + "name": "asset", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "hub", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "accessControlManager_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "isHub", + "inputs": [ + { + "name": "hub", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "registered", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeHub", + "inputs": [ + { + "name": "hub", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setAccessControlManager", + "inputs": [ + { + "name": "accessControlManager_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "HubAdded", + "inputs": [ + { + "name": "asset", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "hub", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "HubRemoved", + "inputs": [ + { + "name": "asset", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "hub", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "NewAccessControlManager", + "inputs": [ + { + "name": "oldAccessControlManager", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "newAccessControlManager", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AssetAlreadyHasHub", + "inputs": [ + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "existingHub", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "HubAlreadyRegistered", + "inputs": [ + { + "name": "hub", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "HubNotRegistered", + "inputs": [ + { + "name": "hub", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "calledContract", + "type": "address", + "internalType": "address" + }, + { + "name": "methodSignature", + "type": "string", + "internalType": "string" + } + ] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [] + } +] diff --git a/simulations/vip-680/bsctestnet-addendum.ts b/simulations/vip-680/bsctestnet-addendum.ts deleted file mode 100644 index bf9c117ef..000000000 --- a/simulations/vip-680/bsctestnet-addendum.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { expect } from "chai"; -import { Contract } from "ethers"; -import { ethers } from "hardhat"; -import { NETWORK_ADDRESSES } from "src/networkAddresses"; -import { expectEvents } from "src/utils"; -import { forking, testVip } from "src/vip-framework"; - -import { - CORE_SOURCE_GOVERNANCE_SIGS, - CORE_SOURCE_USDT, - HUB_GOVERNANCE_SIGS, - HUB_USDT, -} from "../../vips/vip-680/bsctestnet"; -import vip680Addendum from "../../vips/vip-680/bsctestnet-addendum"; -import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; - -const { FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, ACCESS_CONTROL_MANAGER } = NETWORK_ADDRESSES.bsctestnet; - -const FAST_LANE_TIMELOCKS = [FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; - -// bsctestnet block after the Liquidity Hub (USDT) deployment (deploy block ~117918419). -const BLOCK_NUMBER = 117930000; - -const roleId = (contract: string, sig: string) => - ethers.utils.solidityKeccak256(["address", "string"], [contract, sig]); - -// reallocate is the Operator-only role — it is NOT part of the Governance set this addendum grants, so -// the fast-lane timelocks must never receive it. -const REALLOCATE_SIG = "reallocate((address,address,uint256)[],(address,address,uint256)[])"; - -forking(BLOCK_NUMBER, async () => { - let acm: Contract; - - before(async () => { - acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACCESS_CONTROL_MANAGER); - }); - - describe("Pre-VIP state", () => { - it("Fast-Track and Critical timelocks hold no governance role on Hub or Core source", async () => { - for (const timelock of FAST_LANE_TIMELOCKS) { - for (const sig of HUB_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), timelock)).to.equal( - false, - `pre Hub gov ${sig} @ ${timelock}`, - ); - } - for (const sig of CORE_SOURCE_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), timelock)).to.equal( - false, - `pre Core gov ${sig} @ ${timelock}`, - ); - } - } - }); - }); - - testVip( - "VIP-680 [BNB Testnet] Liquidity Hub (USDT) Fast-Track/Critical permissions (addendum)", - await vip680Addendum(), - { - callbackAfterExecution: async txResponse => { - // Governance set (18 Hub + 11 Core = 29) granted to 2 timelocks = 58 RoleGranted events. - await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [58]); - }, - }, - ); - - describe("Post-VIP permissions", () => { - it("Fast-Track and Critical timelocks hold the full governance role set on Hub and Core source", async () => { - for (const timelock of FAST_LANE_TIMELOCKS) { - for (const sig of HUB_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), timelock)).to.equal( - true, - `post Hub gov ${sig} @ ${timelock}`, - ); - } - for (const sig of CORE_SOURCE_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), timelock)).to.equal( - true, - `post Core gov ${sig} @ ${timelock}`, - ); - } - } - }); - - it("Fast-Track and Critical timelocks do NOT hold the operator-only reallocate role", async () => { - for (const timelock of FAST_LANE_TIMELOCKS) { - expect(await acm.hasRole(roleId(HUB_USDT, REALLOCATE_SIG), timelock)).to.equal( - false, - `reallocate @ ${timelock}`, - ); - } - }); - }); -}); diff --git a/simulations/vip-680/bsctestnet-critical.ts b/simulations/vip-680/bsctestnet-critical.ts new file mode 100644 index 000000000..516400517 --- /dev/null +++ b/simulations/vip-680/bsctestnet-critical.ts @@ -0,0 +1,74 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { expectEvents } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import { + ACM, + CORE_SOURCE_USDT, + CRITICAL_TIMELOCK, + FLUX_SOURCE_USDT, + FRV_SOURCE_USDT, + HUB_REGISTRY, + HUB_USDT, +} from "../../vips/vip-680/addresses"; +import vip680Critical from "../../vips/vip-680/bsctestnet-critical"; +import { + CORE_FLUX_GOVERNANCE, + FRV_GOVERNANCE, + HUB_GOVERNANCE, + HUB_REGISTRY_GOVERNANCE, + REALLOCATE, +} from "../../vips/vip-680/permissions"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; + +// bsctestnet block after the Liquidity Hub (USDT) redeployment and the FRV vault creation. +const BLOCK_NUMBER = 119474506; + +const roleId = (contract: string, sig: string) => + ethers.utils.solidityKeccak256(["address", "string"], [contract, sig]); + +// The 51 (contract, sig) pairs granted to the Critical timelock: 18 Hub + 11 Core + 9 FRV + 11 Flux + +// 2 Registry. +const GOV_GRANTS: [string, string][] = [ + ...HUB_GOVERNANCE.map((s): [string, string] => [HUB_USDT, s]), + ...CORE_FLUX_GOVERNANCE.map((s): [string, string] => [CORE_SOURCE_USDT, s]), + ...FRV_GOVERNANCE.map((s): [string, string] => [FRV_SOURCE_USDT, s]), + ...CORE_FLUX_GOVERNANCE.map((s): [string, string] => [FLUX_SOURCE_USDT, s]), + ...HUB_REGISTRY_GOVERNANCE.map((s): [string, string] => [HUB_REGISTRY, s]), +]; + +forking(BLOCK_NUMBER, async () => { + let acm: Contract; + + before(async () => { + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACM); + }); + + describe("Pre-VIP state", () => { + it("Critical timelock holds none of the governance roles this proposal grants", async () => { + for (const [c, s] of GOV_GRANTS) { + expect(await acm.hasRole(roleId(c, s), CRITICAL_TIMELOCK)).to.equal(false, `pre ${c} ${s}`); + } + }); + }); + + testVip("VIP-680 [BNB Testnet] Liquidity Hub (USDT) Critical timelock permissions", await vip680Critical(), { + callbackAfterExecution: async txResponse => { + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [51]); + }, + }); + + describe("Post-VIP state", () => { + it("Critical timelock holds the full governance set across the stack", async () => { + for (const [c, s] of GOV_GRANTS) { + expect(await acm.hasRole(roleId(c, s), CRITICAL_TIMELOCK)).to.equal(true, `post ${c} ${s}`); + } + }); + + it("Critical timelock does NOT hold the operator-only reallocate role", async () => { + expect(await acm.hasRole(roleId(HUB_USDT, REALLOCATE), CRITICAL_TIMELOCK)).to.equal(false); + }); + }); +}); diff --git a/simulations/vip-680/bsctestnet-fast-track.ts b/simulations/vip-680/bsctestnet-fast-track.ts new file mode 100644 index 000000000..6fe14e16d --- /dev/null +++ b/simulations/vip-680/bsctestnet-fast-track.ts @@ -0,0 +1,74 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { expectEvents } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import { + ACM, + CORE_SOURCE_USDT, + FAST_TRACK_TIMELOCK, + FLUX_SOURCE_USDT, + FRV_SOURCE_USDT, + HUB_REGISTRY, + HUB_USDT, +} from "../../vips/vip-680/addresses"; +import vip680FastTrack from "../../vips/vip-680/bsctestnet-fast-track"; +import { + CORE_FLUX_GOVERNANCE, + FRV_GOVERNANCE, + HUB_GOVERNANCE, + HUB_REGISTRY_GOVERNANCE, + REALLOCATE, +} from "../../vips/vip-680/permissions"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; + +// bsctestnet block after the Liquidity Hub (USDT) redeployment and the FRV vault creation. +const BLOCK_NUMBER = 119474506; + +const roleId = (contract: string, sig: string) => + ethers.utils.solidityKeccak256(["address", "string"], [contract, sig]); + +// The 51 (contract, sig) pairs granted to the Fast-Track timelock: 18 Hub + 11 Core + 9 FRV + 11 Flux +// + 2 Registry. +const GOV_GRANTS: [string, string][] = [ + ...HUB_GOVERNANCE.map((s): [string, string] => [HUB_USDT, s]), + ...CORE_FLUX_GOVERNANCE.map((s): [string, string] => [CORE_SOURCE_USDT, s]), + ...FRV_GOVERNANCE.map((s): [string, string] => [FRV_SOURCE_USDT, s]), + ...CORE_FLUX_GOVERNANCE.map((s): [string, string] => [FLUX_SOURCE_USDT, s]), + ...HUB_REGISTRY_GOVERNANCE.map((s): [string, string] => [HUB_REGISTRY, s]), +]; + +forking(BLOCK_NUMBER, async () => { + let acm: Contract; + + before(async () => { + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACM); + }); + + describe("Pre-VIP state", () => { + it("Fast-Track timelock holds none of the governance roles this proposal grants", async () => { + for (const [c, s] of GOV_GRANTS) { + expect(await acm.hasRole(roleId(c, s), FAST_TRACK_TIMELOCK)).to.equal(false, `pre ${c} ${s}`); + } + }); + }); + + testVip("VIP-680 [BNB Testnet] Liquidity Hub (USDT) Fast-Track timelock permissions", await vip680FastTrack(), { + callbackAfterExecution: async txResponse => { + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [51]); + }, + }); + + describe("Post-VIP state", () => { + it("Fast-Track timelock holds the full governance set across the stack", async () => { + for (const [c, s] of GOV_GRANTS) { + expect(await acm.hasRole(roleId(c, s), FAST_TRACK_TIMELOCK)).to.equal(true, `post ${c} ${s}`); + } + }); + + it("Fast-Track timelock does NOT hold the operator-only reallocate role", async () => { + expect(await acm.hasRole(roleId(HUB_USDT, REALLOCATE), FAST_TRACK_TIMELOCK)).to.equal(false); + }); + }); +}); diff --git a/simulations/vip-680/bsctestnet-guardian.ts b/simulations/vip-680/bsctestnet-guardian.ts index 52765daae..1564a53aa 100644 --- a/simulations/vip-680/bsctestnet-guardian.ts +++ b/simulations/vip-680/bsctestnet-guardian.ts @@ -1,92 +1,81 @@ import { expect } from "chai"; import { Contract } from "ethers"; import { ethers } from "hardhat"; -import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { expectEvents } from "src/utils"; import { forking, testVip } from "src/vip-framework"; import { - CORE_SOURCE_OPERATOR_SIGS, + ACM, CORE_SOURCE_USDT, FLUX_SOURCE_USDT, - FRV_SOURCE_GOVERNANCE_SIGS, FRV_SOURCE_USDT, - HUB_OPERATOR_SIGS, + GUARDIAN, + HUB_REGISTRY, HUB_USDT, -} from "../../vips/vip-680/bsctestnet"; -import vip680Guardian, { - CORE_SOURCE_GOVERNANCE_ONLY_SIGS, - FLUX_SOURCE_GOVERNANCE_SIGS, - HUB_GOVERNANCE_ONLY_SIGS, -} from "../../vips/vip-680/bsctestnet-guardian"; +} from "../../vips/vip-680/addresses"; +import vip680Guardian from "../../vips/vip-680/bsctestnet-guardian"; +import { + CORE_FLUX_GOVERNANCE, + FRV_GOVERNANCE, + HUB_FULL, + HUB_REGISTRY_GOVERNANCE, + REALLOCATE, +} from "../../vips/vip-680/permissions"; import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; -const { GUARDIAN, ACCESS_CONTROL_MANAGER } = NETWORK_ADDRESSES.bsctestnet; - -// bsctestnet block after the Liquidity Hub (USDT) deployment (deploy block ~117918419). -const BLOCK_NUMBER = 117930000; +// bsctestnet block after the Liquidity Hub (USDT) redeployment and the FRV vault creation. +const BLOCK_NUMBER = 119474506; -// The exact ACM role id `giveCallPermission(contract, sig, account)` grants: -// keccak256(abi.encodePacked(contract, functionSig)). Asserting `hasRole` on this specific role is -// immune to the `isAllowedToCall` wildcard fallback (a role granted against address(0)), so pre-VIP -// "not granted" checks cannot be masked by an unrelated wildcard permission. const roleId = (contract: string, sig: string) => ethers.utils.solidityKeccak256(["address", "string"], [contract, sig]); -// Every (contract, sig) pair this proposal grants to the Guardian. 12 + 7 + 9 + 11 = 39. +// Every (contract, sig) pair this testnet-only proposal grants to the Guardian: 19 Hub (18 governance + +// operator-only reallocate) + 11 Core + 9 FRV + 11 Flux + 2 Registry = 52. const GUARDIAN_GRANTS: [string, string][] = [ - ...HUB_GOVERNANCE_ONLY_SIGS.map((sig): [string, string] => [HUB_USDT, sig]), - ...CORE_SOURCE_GOVERNANCE_ONLY_SIGS.map((sig): [string, string] => [CORE_SOURCE_USDT, sig]), - ...FRV_SOURCE_GOVERNANCE_SIGS.map((sig): [string, string] => [FRV_SOURCE_USDT, sig]), - ...FLUX_SOURCE_GOVERNANCE_SIGS.map((sig): [string, string] => [FLUX_SOURCE_USDT, sig]), + ...HUB_FULL.map((s): [string, string] => [HUB_USDT, s]), + ...CORE_FLUX_GOVERNANCE.map((s): [string, string] => [CORE_SOURCE_USDT, s]), + ...FRV_GOVERNANCE.map((s): [string, string] => [FRV_SOURCE_USDT, s]), + ...CORE_FLUX_GOVERNANCE.map((s): [string, string] => [FLUX_SOURCE_USDT, s]), + ...HUB_REGISTRY_GOVERNANCE.map((s): [string, string] => [HUB_REGISTRY, s]), ]; forking(BLOCK_NUMBER, async () => { let acm: Contract; before(async () => { - acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACCESS_CONTROL_MANAGER); + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACM); }); describe("Pre-VIP state", () => { - it("this proposal grants exactly 39 (contract, sig) pairs to the Guardian", () => { - // 12 Hub gov-only + 7 Core gov-only + 9 FRV + 11 Flux. - expect(GUARDIAN_GRANTS.length).to.equal(39); + it("this proposal grants exactly 52 (contract, sig) pairs to the Guardian", () => { + expect(GUARDIAN_GRANTS.length).to.equal(52); }); - it("Guardian holds none of the governance roles this proposal grants", async () => { - for (const [contract, sig] of GUARDIAN_GRANTS) { - expect(await acm.hasRole(roleId(contract, sig), GUARDIAN)).to.equal(false, `pre ${contract} ${sig}`); - } - }); - - it("Guardian holds no role on the FRV or Flux sources yet", async () => { - for (const sig of FRV_SOURCE_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(FRV_SOURCE_USDT, sig), GUARDIAN)).to.equal(false, `pre FRV ${sig}`); - } - for (const sig of FLUX_SOURCE_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(FLUX_SOURCE_USDT, sig), GUARDIAN)).to.equal(false, `pre Flux ${sig}`); + it("Guardian holds none of the roles this proposal grants", async () => { + for (const [c, s] of GUARDIAN_GRANTS) { + expect(await acm.hasRole(roleId(c, s), GUARDIAN)).to.equal(false, `pre ${c} ${s}`); } }); }); testVip("VIP-680 [BNB Testnet] Liquidity Hub (USDT) Guardian full permissions", await vip680Guardian(), { callbackAfterExecution: async txResponse => { - // 12 Hub gov-only + 7 Core gov-only + 9 FRV + 11 Flux = 39 RoleGranted events. - await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [39]); + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [52]); }, }); - describe("Post-VIP permissions", () => { - it("Guardian now holds every governance role this proposal grants", async () => { - for (const [contract, sig] of GUARDIAN_GRANTS) { - expect(await acm.hasRole(roleId(contract, sig), GUARDIAN)).to.equal(true, `post ${contract} ${sig}`); + describe("Post-VIP state", () => { + it("Guardian now holds every role this proposal grants", async () => { + for (const [c, s] of GUARDIAN_GRANTS) { + expect(await acm.hasRole(roleId(c, s), GUARDIAN)).to.equal(true, `post ${c} ${s}`); } }); - it("Guardian can now add resources / yield groups (loosening) on every target", async () => { - // The whole point of this testnet proposal: the Guardian holds the route-creating (governance) - // roles the mainnet asymmetric model reserves for the timelocks. + it("Guardian holds the operator-only reallocate role on the Hub (full set, testnet)", async () => { + expect(await acm.hasRole(roleId(HUB_USDT, REALLOCATE), GUARDIAN)).to.equal(true); + }); + + it("Guardian can loosen on every target (the point of this testnet proposal)", async () => { expect(await acm.hasRole(roleId(HUB_USDT, "addYieldGroup(address,uint256,uint16)"), GUARDIAN)).to.equal(true); for (const source of [CORE_SOURCE_USDT, FRV_SOURCE_USDT, FLUX_SOURCE_USDT]) { expect(await acm.hasRole(roleId(source, "addResource(address,address)"), GUARDIAN)).to.equal( @@ -94,21 +83,7 @@ forking(BLOCK_NUMBER, async () => { `addResource @ ${source}`, ); } - }); - - it("does not grant the operator-only Hub roles (those come from the main VIP-680 proposal)", async () => { - // This file grants only the Governance(-only) signatures. The Operator/tighten set — including - // the operator-only `reallocate` and the shared queue/pause signatures — is granted to the - // Guardian by the main VIP-680 proposal, so on this standalone fork it stays ungranted here. - for (const sig of HUB_OPERATOR_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), GUARDIAN)).to.equal(false, `Hub op not granted here: ${sig}`); - } - for (const sig of CORE_SOURCE_OPERATOR_SIGS) { - expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), GUARDIAN)).to.equal( - false, - `Core op not granted here: ${sig}`, - ); - } + expect(await acm.hasRole(roleId(HUB_REGISTRY, "addHub(address)"), GUARDIAN)).to.equal(true); }); }); }); diff --git a/simulations/vip-680/bsctestnet-wiring.ts b/simulations/vip-680/bsctestnet-wiring.ts new file mode 100644 index 000000000..f41a6a749 --- /dev/null +++ b/simulations/vip-680/bsctestnet-wiring.ts @@ -0,0 +1,160 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { expectEvents } from "src/utils"; +import { forking, pretendExecutingVip, testVip } from "src/vip-framework"; + +import { + ABSOLUTE_CAP_UNBOUNDED, + ACM, + ADAPTER_CORE_V1, + ADAPTER_FLUX, + ADAPTER_FRV, + CORE_SOURCE_USDT, + FLUX_SOURCE_USDT, + FRV_SOURCE_USDT, + FRV_VAULT_USDT, + FUSDT_FLUX, + HUB_REGISTRY, + HUB_USDT, + NORMAL_TIMELOCK, + PERCENTAGE_CAP_DISABLED, + USDT, + VUSDT_CORE, +} from "../../vips/vip-680/addresses"; +import vip680 from "../../vips/vip-680/bsctestnet"; +import vip680Wiring from "../../vips/vip-680/bsctestnet-wiring"; +import HUB_ABI from "./abi/Hub.json"; +import HUB_REGISTRY_ABI from "./abi/HubRegistry.json"; +import YIELD_GROUP_ABI from "./abi/YieldGroup.json"; + +// bsctestnet block after the Liquidity Hub (USDT) redeployment and the FRV vault creation. +const BLOCK_NUMBER = 119474506; + +const addr = (a: string) => ethers.utils.getAddress(a); + +const OUTER_QUEUE = [FRV_SOURCE_USDT, FLUX_SOURCE_USDT, CORE_SOURCE_USDT]; + +forking(BLOCK_NUMBER, async () => { + let hub: Contract; + let registry: Contract; + let core: Contract; + let frv: Contract; + let flux: Contract; + + before(async () => { + // This wiring proposal needs the roles + ownership from the main VIP-680 proposal. Replay it as the + // Normal Timelock so the fixture snapshot reflects the on-chain state after it executes. + await pretendExecutingVip(await vip680(), NORMAL_TIMELOCK); + + hub = await ethers.getContractAt(HUB_ABI, HUB_USDT); + registry = await ethers.getContractAt(HUB_REGISTRY_ABI, HUB_REGISTRY); + core = await ethers.getContractAt(YIELD_GROUP_ABI, CORE_SOURCE_USDT); + frv = await ethers.getContractAt(YIELD_GROUP_ABI, FRV_SOURCE_USDT); + flux = await ethers.getContractAt(YIELD_GROUP_ABI, FLUX_SOURCE_USDT); + }); + + describe("Pre-wiring state (after main VIP-680)", () => { + it("Normal Timelock owns the Hub and registry (main VIP-680 accepted ownership)", async () => { + expect(await hub.owner()).to.equal(NORMAL_TIMELOCK); + expect(await registry.owner()).to.equal(NORMAL_TIMELOCK); + }); + + it("Hub unregistered, no yield groups, sources empty", async () => { + expect(await registry.isHub(HUB_USDT)).to.equal(false); + expect(await registry.getHubs()).to.deep.equal([]); + expect(await hub.registeredYieldGroups()).to.deep.equal([]); + for (const src of [core, frv, flux]) { + expect(await src.resources()).to.deep.equal([]); + } + }); + + it("Hub advertises zero deposit capacity (no routes wired)", async () => { + expect(await hub.maxDeposit(NORMAL_TIMELOCK)).to.equal(0); + }); + }); + + testVip("VIP-680 [BNB Testnet] Liquidity Hub (USDT) wiring", await vip680Wiring(), { + callbackAfterExecution: async txResponse => { + await expectEvents(txResponse, [HUB_REGISTRY_ABI], ["HubAdded"], [1]); + await expectEvents( + txResponse, + [HUB_ABI], + ["YieldGroupAdded", "OuterDepositQueueSet", "OuterWithdrawQueueSet"], + [3, 1, 1], + ); + await expectEvents( + txResponse, + [YIELD_GROUP_ABI], + ["ResourceAdded", "InnerDepositQueueSet", "InnerWithdrawQueueSet"], + [3, 3, 3], + ); + + // README invariant: HubAdded lands at a lower log index than every YieldGroupAdded, so indexers + // seed each Hub's yield-group set correctly. + const receipt = await txResponse.wait(); + const topicHubAdded = new ethers.utils.Interface(HUB_REGISTRY_ABI).getEventTopic("HubAdded"); + const topicYieldGroupAdded = new ethers.utils.Interface(HUB_ABI).getEventTopic("YieldGroupAdded"); + const hubAddedIdx = receipt.logs.filter(l => l.topics[0] === topicHubAdded).map(l => l.logIndex); + const yieldGroupAddedIdx = receipt.logs.filter(l => l.topics[0] === topicYieldGroupAdded).map(l => l.logIndex); + expect(hubAddedIdx.length).to.equal(1); + expect(yieldGroupAddedIdx.length).to.equal(3); + expect(hubAddedIdx[0]).to.be.lessThan(Math.min(...yieldGroupAddedIdx)); + }, + }); + + describe("Post-wiring state", () => { + it("Hub is registered in the HubRegistry", async () => { + expect(await registry.isHub(HUB_USDT)).to.equal(true); + expect((await registry.getHubsCount()).toNumber()).to.equal(1); + expect(addr(await registry.hubForAsset(USDT))).to.equal(addr(HUB_USDT)); + }); + + it("three yield groups registered on the Hub, all uncapped", async () => { + expect((await hub.registeredYieldGroups()).map(addr)).to.have.members( + [CORE_SOURCE_USDT, FRV_SOURCE_USDT, FLUX_SOURCE_USDT].map(addr), + ); + for (const src of [CORE_SOURCE_USDT, FRV_SOURCE_USDT, FLUX_SOURCE_USDT]) { + const cfg = await hub.yieldGroupConfig(src); + expect(cfg.registered).to.equal(true); + expect(cfg.paused).to.equal(false); + expect(cfg.absoluteCap.toString()).to.equal(ABSOLUTE_CAP_UNBOUNDED); + expect(cfg.percentageCapBps).to.equal(PERCENTAGE_CAP_DISABLED); + } + }); + + it("Hub outer queues route [FRV, Flux, Core] for deposits and withdrawals", async () => { + const want = OUTER_QUEUE.map(addr); + expect((await hub.outerDepositQueue()).map(addr)).to.deep.equal(want); + expect((await hub.outerWithdrawQueue()).map(addr)).to.deep.equal(want); + }); + + it("each source holds its resource behind the right adapter, with matching inner queues", async () => { + const cases: [Contract, string, string][] = [ + [core, VUSDT_CORE, ADAPTER_CORE_V1], + [frv, FRV_VAULT_USDT, ADAPTER_FRV], + [flux, FUSDT_FLUX, ADAPTER_FLUX], + ]; + for (const [src, resource, adapter] of cases) { + expect((await src.resources()).map(addr)).to.deep.equal([addr(resource)]); + const [registered, paused, boundAdapter] = await src.resourceConfig(resource); + expect(registered).to.equal(true); + expect(paused).to.equal(false); + expect(addr(boundAdapter)).to.equal(addr(adapter)); + expect((await src.innerDepositQueue()).map(addr)).to.deep.equal([addr(resource)]); + expect((await src.innerWithdrawQueue()).map(addr)).to.deep.equal([addr(resource)]); + } + }); + + it("accessControlManager is the canonical ACM on all five contracts", async () => { + for (const c of [hub, registry, core, frv, flux]) { + expect(addr(await c.accessControlManager())).to.equal(addr(ACM)); + } + }); + + it("Hub now advertises deposit capacity through the wired route", async () => { + // maxDeposit walks the whole live route: Hub outer queue -> source inner queue -> adapter.maxDeposit. + expect(await hub.maxDeposit(NORMAL_TIMELOCK)).to.be.gt(0); + }); + }); +}); diff --git a/simulations/vip-680/bsctestnet.ts b/simulations/vip-680/bsctestnet.ts index 8c5a35b3c..007ad5c78 100644 --- a/simulations/vip-680/bsctestnet.ts +++ b/simulations/vip-680/bsctestnet.ts @@ -1,201 +1,116 @@ import { expect } from "chai"; import { Contract } from "ethers"; import { ethers } from "hardhat"; -import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { expectEvents } from "src/utils"; import { forking, testVip } from "src/vip-framework"; -import vip680, { - ADAPTER_CORE_V1, - CORE_ABSOLUTE_CAP, - CORE_SOURCE_GOVERNANCE_SIGS, - CORE_SOURCE_OPERATOR_SIGS, +import { + ACM, CORE_SOURCE_USDT, - HUB_GOVERNANCE_SIGS, - HUB_OPERATOR_SIGS, + CRITICAL_TIMELOCK, + FAST_TRACK_TIMELOCK, + FLUX_SOURCE_USDT, + FRV_SOURCE_USDT, + GUARDIAN, + HUB_REGISTRY, HUB_USDT, - PERCENTAGE_CAP_DISABLED, - VUSDT_CORE, -} from "../../vips/vip-680/bsctestnet"; + NORMAL_TIMELOCK, +} from "../../vips/vip-680/addresses"; +import vip680 from "../../vips/vip-680/bsctestnet"; +import { + CORE_FLUX_GOVERNANCE, + FRV_GOVERNANCE, + HUB_GOVERNANCE, + HUB_REGISTRY_GOVERNANCE, + REALLOCATE, +} from "../../vips/vip-680/permissions"; import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; import HUB_ABI from "./abi/Hub.json"; -import YIELD_GROUP_ABI from "./abi/YieldGroup.json"; +import HUB_REGISTRY_ABI from "./abi/HubRegistry.json"; -const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN, ACCESS_CONTROL_MANAGER } = - NETWORK_ADDRESSES.bsctestnet; +// bsctestnet block after the Liquidity Hub (USDT) redeployment and the FRV vault creation. +const BLOCK_NUMBER = 119474506; -// The Fast-Track / Critical timelocks receive their governance grants in the addendum proposal, so -// they must stay ungranted here — this proves the split boundary. -const FAST_LANE_TIMELOCKS = [FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; - -// bsctestnet block after the Liquidity Hub (USDT) deployment (deploy block ~117918419). -const BLOCK_NUMBER = 117930000; - -// The exact ACM role id `giveCallPermission(contract, sig, account)` grants: -// keccak256(abi.encodePacked(contract, functionSig)). Asserting `hasRole` on this specific role is -// immune to the `isAllowedToCall` wildcard fallback (a role granted against address(0)), so pre-VIP -// "not granted" checks cannot be masked by an unrelated wildcard permission. +// The exact ACM role id giveCallPermission(contract, sig, account) grants: +// keccak256(abi.encodePacked(contract, functionSig)). Asserting hasRole on this specific role is immune +// to the isAllowedToCall wildcard fallback, so pre-VIP "not granted" checks cannot be masked. const roleId = (contract: string, sig: string) => ethers.utils.solidityKeccak256(["address", "string"], [contract, sig]); -const addr = (a: string) => ethers.utils.getAddress(a); - -// Governance-only signatures = governance set minus the shared (also-Operator) tighten actions. -const HUB_GOVERNANCE_ONLY_SIGS = HUB_GOVERNANCE_SIGS.filter(s => !HUB_OPERATOR_SIGS.includes(s)); -const CORE_SOURCE_GOVERNANCE_ONLY_SIGS = CORE_SOURCE_GOVERNANCE_SIGS.filter( - s => !CORE_SOURCE_OPERATOR_SIGS.includes(s), -); - -const REALLOCATE_SIG = "reallocate((address,address,uint256)[],(address,address,uint256)[])"; +// Every (contract, sig) pair this proposal grants to the Normal Timelock: 18 Hub + 11 Core + 9 FRV + +// 11 Flux + 2 Registry = 51. +const GOV_GRANTS: [string, string][] = [ + ...HUB_GOVERNANCE.map((s): [string, string] => [HUB_USDT, s]), + ...CORE_FLUX_GOVERNANCE.map((s): [string, string] => [CORE_SOURCE_USDT, s]), + ...FRV_GOVERNANCE.map((s): [string, string] => [FRV_SOURCE_USDT, s]), + ...CORE_FLUX_GOVERNANCE.map((s): [string, string] => [FLUX_SOURCE_USDT, s]), + ...HUB_REGISTRY_GOVERNANCE.map((s): [string, string] => [HUB_REGISTRY, s]), +]; forking(BLOCK_NUMBER, async () => { let acm: Contract; let hub: Contract; - let coreSource: Contract; + let registry: Contract; before(async () => { - acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACCESS_CONTROL_MANAGER); + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACM); hub = await ethers.getContractAt(HUB_ABI, HUB_USDT); - coreSource = await ethers.getContractAt(YIELD_GROUP_ABI, CORE_SOURCE_USDT); + registry = await ethers.getContractAt(HUB_REGISTRY_ABI, HUB_REGISTRY); }); describe("Pre-VIP state", () => { - it("Hub has no registered yield groups and empty outer queues", async () => { - expect(await hub.registeredYieldGroups()).to.deep.equal([]); - expect(await hub.outerDepositQueue()).to.deep.equal([]); - expect(await hub.outerWithdrawQueue()).to.deep.equal([]); - }); - - it("Core source has no registered resources and empty inner queues", async () => { - expect(await coreSource.resources()).to.deep.equal([]); - expect(await coreSource.innerDepositQueue()).to.deep.equal([]); - expect(await coreSource.innerWithdrawQueue()).to.deep.equal([]); - }); - - it("Hub advertises zero deposit capacity (no routes wired)", async () => { - expect(await hub.maxDeposit(NORMAL_TIMELOCK)).to.equal(0); + it("GOV_GRANTS is the expected 51 (contract, sig) pairs", () => { + expect(GOV_GRANTS.length).to.equal(51); }); - it("Normal timelock holds no governance role on Hub or Core source", async () => { - for (const sig of HUB_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), NORMAL_TIMELOCK)).to.equal(false, `pre Hub gov ${sig}`); - } - for (const sig of CORE_SOURCE_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), NORMAL_TIMELOCK)).to.equal( - false, - `pre Core gov ${sig}`, - ); - } + it("Hub and HubRegistry are pending-owned by the Normal Timelock (not yet accepted)", async () => { + expect(await hub.pendingOwner()).to.equal(NORMAL_TIMELOCK); + expect(await registry.pendingOwner()).to.equal(NORMAL_TIMELOCK); + expect(await hub.owner()).to.not.equal(NORMAL_TIMELOCK); + expect(await registry.owner()).to.not.equal(NORMAL_TIMELOCK); }); - it("Guardian holds no operator role on Hub or Core source", async () => { - for (const sig of HUB_OPERATOR_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), GUARDIAN)).to.equal(false, `pre Hub op ${sig}`); - } - for (const sig of CORE_SOURCE_OPERATOR_SIGS) { - expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), GUARDIAN)).to.equal(false, `pre Core op ${sig}`); + it("Normal Timelock holds none of the governance roles this proposal grants", async () => { + for (const [c, s] of GOV_GRANTS) { + expect(await acm.hasRole(roleId(c, s), NORMAL_TIMELOCK)).to.equal(false, `pre ${c} ${s}`); } }); }); - testVip("VIP-680 [BNB Testnet] Configure Liquidity Hub (USDT)", await vip680(), { + testVip("VIP-680 [BNB Testnet] Liquidity Hub (USDT) Normal Timelock permissions", await vip680(), { callbackAfterExecution: async txResponse => { - // Normal-timelock governance: 18 Hub + 11 Core = 29. Guardian operator: 7 Hub + 4 Core = 11. Total 40. - await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [40]); - await expectEvents( - txResponse, - [HUB_ABI], - ["YieldGroupAdded", "OuterDepositQueueSet", "OuterWithdrawQueueSet"], - [1, 1, 1], - ); - await expectEvents( - txResponse, - [YIELD_GROUP_ABI], - ["ResourceAdded", "InnerDepositQueueSet", "InnerWithdrawQueueSet"], - [1, 1, 1], - ); + // 51 governance grants + 2 acceptOwnership (Hub + registry each emit OwnershipTransferred). + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [51]); + await expectEvents(txResponse, [HUB_ABI], ["OwnershipTransferred"], [2]); }, }); - describe("Post-VIP wiring", () => { - it("Core source registered on the Hub with the expected caps", async () => { - expect((await hub.registeredYieldGroups()).map(addr)).to.deep.equal([addr(CORE_SOURCE_USDT)]); - const cfg = await hub.yieldGroupConfig(CORE_SOURCE_USDT); - expect(cfg.registered).to.equal(true); - expect(cfg.paused).to.equal(false); - expect(cfg.absoluteCap.toString()).to.equal(CORE_ABSOLUTE_CAP); - expect(cfg.percentageCapBps).to.equal(PERCENTAGE_CAP_DISABLED); - }); - - it("Hub outer queues route to the Core source", async () => { - expect((await hub.outerDepositQueue()).map(addr)).to.deep.equal([addr(CORE_SOURCE_USDT)]); - expect((await hub.outerWithdrawQueue()).map(addr)).to.deep.equal([addr(CORE_SOURCE_USDT)]); - }); - - it("vUSDT registered on the Core source behind AdapterCoreV1", async () => { - expect((await coreSource.resources()).map(addr)).to.deep.equal([addr(VUSDT_CORE)]); - const [registered, paused, adapter] = await coreSource.resourceConfig(VUSDT_CORE); - expect(registered).to.equal(true); - expect(paused).to.equal(false); - expect(addr(adapter)).to.equal(addr(ADAPTER_CORE_V1)); + describe("Post-VIP state", () => { + it("Hub and HubRegistry are now owned by the Normal Timelock", async () => { + expect(await hub.owner()).to.equal(NORMAL_TIMELOCK); + expect(await registry.owner()).to.equal(NORMAL_TIMELOCK); }); - it("Core source inner queues route to vUSDT", async () => { - expect((await coreSource.innerDepositQueue()).map(addr)).to.deep.equal([addr(VUSDT_CORE)]); - expect((await coreSource.innerWithdrawQueue()).map(addr)).to.deep.equal([addr(VUSDT_CORE)]); - }); - - it("Hub now advertises deposit capacity through Core → vUSDT", async () => { - // Non-zero maxDeposit exercises the whole wired route: Hub outer queue → Core source inner - // queue → AdapterCoreV1.maxDeposit(vUSDT) (mint unpaused, supply-cap headroom). - expect(await hub.maxDeposit(NORMAL_TIMELOCK)).to.be.gt(0); - }); - }); - - describe("Post-VIP permissions", () => { - it("Normal timelock holds the full governance role set on Hub and Core source", async () => { - for (const sig of HUB_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), NORMAL_TIMELOCK)).to.equal(true, `post Hub gov ${sig}`); - } - for (const sig of CORE_SOURCE_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), NORMAL_TIMELOCK)).to.equal( - true, - `post Core gov ${sig}`, - ); + it("Normal Timelock holds the full governance set across the stack", async () => { + for (const [c, s] of GOV_GRANTS) { + expect(await acm.hasRole(roleId(c, s), NORMAL_TIMELOCK)).to.equal(true, `post ${c} ${s}`); } }); - it("Guardian holds the full operator role set on Hub and Core source", async () => { - for (const sig of HUB_OPERATOR_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), GUARDIAN)).to.equal(true, `post Hub op ${sig}`); - } - for (const sig of CORE_SOURCE_OPERATOR_SIGS) { - expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), GUARDIAN)).to.equal(true, `post Core op ${sig}`); - } + it("Normal Timelock does NOT hold the operator-only reallocate role", async () => { + expect(await acm.hasRole(roleId(HUB_USDT, REALLOCATE), NORMAL_TIMELOCK)).to.equal(false); }); - it("Guardian holds NO governance-only role (asymmetric: Operator can only tighten)", async () => { - for (const sig of HUB_GOVERNANCE_ONLY_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), GUARDIAN)).to.equal(false, `Guardian must NOT have Hub ${sig}`); - } - for (const sig of CORE_SOURCE_GOVERNANCE_ONLY_SIGS) { - expect(await acm.hasRole(roleId(CORE_SOURCE_USDT, sig), GUARDIAN)).to.equal( - false, - `Guardian must NOT have Core ${sig}`, - ); + it("Fast-Track, Critical and Guardian hold no governance role yet", async () => { + for (const account of [FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN]) { + for (const [c, s] of GOV_GRANTS) { + expect(await acm.hasRole(roleId(c, s), account)).to.equal(false, `${account} ${c} ${s}`); + } } }); - it("Normal timelock does NOT hold the operator-only reallocate role", async () => { - expect(await acm.hasRole(roleId(HUB_USDT, REALLOCATE_SIG), NORMAL_TIMELOCK)).to.equal(false); - }); - - it("Fast-Track and Critical timelocks are not yet granted (deferred to the addendum)", async () => { - for (const timelock of FAST_LANE_TIMELOCKS) { - for (const sig of HUB_GOVERNANCE_SIGS) { - expect(await acm.hasRole(roleId(HUB_USDT, sig), timelock)).to.equal(false, `${sig} @ ${timelock}`); - } - } + it("Hub still has no registered yield groups (wiring deferred to the wiring proposal)", async () => { + expect(await hub.registeredYieldGroups()).to.deep.equal([]); }); }); }); diff --git a/vips/vip-680/addresses.ts b/vips/vip-680/addresses.ts new file mode 100644 index 000000000..21960b716 --- /dev/null +++ b/vips/vip-680/addresses.ts @@ -0,0 +1,75 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; + +// =================================================================================================== +// VIP-680 [BNB Chain Testnet] — Liquidity Hub (USDT) address book. +// +// One reference for every address the VIP touches. The whole Hub stack (HubRegistry + per-asset Hub + +// Core/FRV/Flux sources + adapters) was REDEPLOYED at ~block 119,459,xxx; the values below are the +// current committed addresses from venus-liquidity-hub/deployments/bsctestnet/*.json. That repo is not +// published as an @venusprotocol/*-deployments npm package, so its addresses are inlined here (source +// file noted per section). Governance / ACM come from @venusprotocol/governance-contracts via +// NETWORK_ADDRESSES — the canonical source — so they are imported, never hardcoded. +// +// Verified on a bsctestnet fork/RPC (2026-07-16): +// - Hub_USDT.asset() == vUSDT.underlying() == fUSDT.asset() == FRV_VAULT.asset() == USDT +// (0xA11c…782c) -> addResource passes its ResourceAssetMismatch guard on every source. +// - Hub_USDT.pendingOwner() == HubRegistry.pendingOwner() == NORMAL_TIMELOCK +// -> both acceptOwnership() calls succeed from the Normal-timelock proposal. +// - config.acm == ACM (0x45f8…); config.governance == NORMAL_TIMELOCK (0xce10…). +// =================================================================================================== + +const { ACCESS_CONTROL_MANAGER, NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN } = + NETWORK_ADDRESSES.bsctestnet; + +// --------------------------------------------------------------------------------------------------- +// Governance / access control (source: @venusprotocol/governance-contracts, via NETWORK_ADDRESSES). +// --------------------------------------------------------------------------------------------------- +export const ACM = ACCESS_CONTROL_MANAGER; // 0x45f8a08F534f34A97187626E05d4b6648Eeaa9AA +export { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN }; +// The Guardian multisig plays the Operator role, and (testnet only) is topped up to full Governance. +export const OPERATOR = GUARDIAN; // 0x2Ce1d0ffD7E869D9DF33e28552b12DdDed326706 + +// --------------------------------------------------------------------------------------------------- +// Hub stack — CALLED by the VIP (source: venus-liquidity-hub/deployments/bsctestnet/*.json). +// --------------------------------------------------------------------------------------------------- +export const HUB_REGISTRY = "0x5346f648029d1D1d1034e09e8AD7a115f5D7A159"; // acceptOwnership + addHub target +export const HUB_USDT = "0x7cE6ADF754D0eC81A6CF8ACd9C7454F45077dc61"; // acceptOwnership + roles + wiring target +export const CORE_SOURCE_USDT = "0x11e39DC7b8b16BBDA8D9C2903dF741Ae9341Ec88"; +export const FRV_SOURCE_USDT = "0xA0Fb0fFeBdcB7F45A3Ec841cCE7F78B7CeBD0f82"; +export const FLUX_SOURCE_USDT = "0x044E572144bc08ed2D90E081EeEd7b5b6Cb01016"; +export const ADAPTER_CORE_V1 = "0xDf669957448eCB23309eEFda4de230c62d22AE33"; +export const ADAPTER_FRV = "0xeF0E85ab9A23F50EB4595CF7e2F5461feF7E7fc5"; +export const ADAPTER_FLUX = "0x15Dca35ae0b16BeceabAEC9Dea49630e8C601730"; + +// Ownable2Step contracts whose ownership was transferred to governance at deploy and must be accepted +// (both are AccessControlledV8 -> Ownable2Step). The beacons and the ProxyAdmin are single-step Ownable +// (already owned by governance, no accept); the source proxies are ACM-only (not Ownable, no owner). +export const OWNERSHIP_ACCEPT_TARGETS = [HUB_USDT, HUB_REGISTRY]; + +// --------------------------------------------------------------------------------------------------- +// Resources registered inside each source — CALLED by the VIP (addResource + inner queues). +// --------------------------------------------------------------------------------------------------- +export const VUSDT_CORE = "0xb7526572FFE56AB9D7489838Bf2E18e3323b441A"; // @venusprotocol/venus-protocol core vUSDT +export const FRV_VAULT_USDT = "0x9F6Edab0123188C852854D2D9601115168f52F7a"; // new FRV vault (asset()==USDT, verified) +export const FUSDT_FLUX = "0x52217232e12A1c906aB8DEf58532a3618970D025"; // Fluid fUSDT fToken (ERC-4626) + +// Underlying asset shared by the Hub and every resource above. +export const USDT = "0xA11c8D9DC9b66E209Ef60F0C8D969D3CD988782c"; + +// --------------------------------------------------------------------------------------------------- +// Hub stack — REFERENCE ONLY (not called by the VIP; owned by governance at deploy). +// --------------------------------------------------------------------------------------------------- +export const HUB_BEACON = "0x7cbaC6991aC33DaFDD347e84CFbE2F372b936d92"; +export const CORE_BEACON = "0xbBEe25aE7d2Db035Afc327fb0096fC88FDfF3170"; +export const FRV_BEACON = "0x6196Ec22133610132563B03b6Fad5aa766A9C037"; +export const FLUX_BEACON = "0x6b9CA74F82848668EA04D56E0A8396A816ba5330"; +export const HUB_REGISTRY_PROXY_ADMIN = "0x9f8413eEE33D434F6D4f40C83181f32A831c9ef7"; +export const MIGRATOR = "0x343D518d8C89f9B5D770000F1ed80f45bF1419f5"; // immutable, permissionless — no wiring + +// --------------------------------------------------------------------------------------------------- +// Cap constants for Hub.addYieldGroup(source, absoluteCap, percentageCapBps). +// --------------------------------------------------------------------------------------------------- +// The Hub rejects type(uint256).max as InvalidCap; type(uint128).max is the canonical "no ceiling". +export const ABSOLUTE_CAP_UNBOUNDED = "340282366920938463463374607431768211455"; // type(uint128).max +// 10_000 bps disables the percentage-of-TVL cap dimension, leaving only the absolute cap binding. +export const PERCENTAGE_CAP_DISABLED = 10_000; diff --git a/vips/vip-680/bsctestnet-addendum.ts b/vips/vip-680/bsctestnet-addendum.ts deleted file mode 100644 index b232e4bf6..000000000 --- a/vips/vip-680/bsctestnet-addendum.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NETWORK_ADDRESSES } from "src/networkAddresses"; -import { ProposalType } from "src/types"; -import { makeProposal } from "src/utils"; - -import { - CORE_SOURCE_GOVERNANCE_SIGS, - CORE_SOURCE_USDT, - HUB_GOVERNANCE_SIGS, - HUB_USDT, - giveCallPermission, -} from "./bsctestnet"; - -const { FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK } = NETWORK_ADDRESSES.bsctestnet; - -// Addendum to VIP-680. The main proposal grants the Liquidity Hub (USDT) Governance role set to the -// Normal timelock (plus the Operator set to the Guardian) and performs the Core wiring. This addendum -// grants the SAME Governance role set on Hub_USDT and CoreSource_USDT to the Fast-Track and Critical -// timelocks, so urgent proposals can operate the Hub too. It is a separate proposal because all three -// timelocks' governance grants plus the wiring exceed a single proposal's block-gas budget on BSC. -// Addresses, ACM target, and role strings are imported from the main VIP-680 file as the single -// source of truth. -const FAST_LANE_TIMELOCKS = [FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; - -export const vip680Addendum = () => { - const meta = { - version: "v2", - title: "VIP-680 [BNB Chain Testnet] Liquidity Hub (USDT) — Fast-Track and Critical timelock permissions (addendum)", - description: `#### Summary - -Addendum to VIP-680. Grants the **Governance** role set on the **Liquidity Hub (USDT)** and its -**Core yield source** to the **Fast-Track** and **Critical** timelocks on BNB Chain Testnet, so -urgent (fast-track / critical) proposals can operate the Hub in addition to the Normal timelock -configured in the main VIP-680 proposal. - -#### Actions - -Grant, on both **Hub_USDT** and the **Core source**, the full Governance role set (registry, caps, -queues, pause/unpause, fees, sweep, adapter updates, \`emergencyReallocate\`) to the Fast-Track and -Critical timelocks. - -#### Notes - -- This proposal grants timelock roles only. The Guardian's permissions are set separately: the - Operator (tighten-only) set by the main VIP-680 proposal, and the full Governance set by the - companion Guardian-permissions proposal (testnet-only). -- Testnet-only. The Hub is not yet deployed on any mainnet.`, - forDescription: "Execute this proposal", - againstDescription: "Do not execute this proposal", - abstainDescription: "Indifferent to execution", - }; - - return makeProposal( - [ - ...FAST_LANE_TIMELOCKS.flatMap(timelock => - HUB_GOVERNANCE_SIGS.map(sig => giveCallPermission(HUB_USDT, sig, timelock)), - ), - ...FAST_LANE_TIMELOCKS.flatMap(timelock => - CORE_SOURCE_GOVERNANCE_SIGS.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, timelock)), - ), - ], - meta, - ProposalType.REGULAR, - ); -}; - -export default vip680Addendum; diff --git a/vips/vip-680/bsctestnet-critical.ts b/vips/vip-680/bsctestnet-critical.ts new file mode 100644 index 000000000..6ce604974 --- /dev/null +++ b/vips/vip-680/bsctestnet-critical.ts @@ -0,0 +1,71 @@ +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { + CORE_SOURCE_USDT, + CRITICAL_TIMELOCK, + FLUX_SOURCE_USDT, + FRV_SOURCE_USDT, + HUB_REGISTRY, + HUB_USDT, +} from "./addresses"; +import { + CORE_FLUX_GOVERNANCE, + FRV_GOVERNANCE, + HUB_GOVERNANCE, + HUB_REGISTRY_GOVERNANCE, + giveCallPermission, +} from "./permissions"; + +// --------------------------------------------------------------------------------------------------- +// VIP-680 (Critical addendum) — BNB Chain Testnet. +// +// Grants the same Governance role set the main proposal gives the Normal Timelock to the Critical +// timelock, across the whole stack (Hub + Core + FRV + Flux + HubRegistry), so critical proposals can +// operate the Hub too. Separate proposal for the same block-gas reason as the Fast-Track addendum. +// +// NOTE: full-Governance parity with the Normal Timelock is a testnet choice; on mainnet the fast lanes +// would instead receive only a tightening subset. Do not mirror this full-set grant to mainnet. +// --------------------------------------------------------------------------------------------------- + +export const vip680Critical = () => { + const meta = { + version: "v2", + title: "VIP-680 [BNB Chain Testnet] Liquidity Hub (USDT) — Critical timelock permissions (addendum)", + description: `#### Summary + +Fourth of five proposals onboarding the redeployed **Liquidity Hub (USDT)** on BNB Chain Testnet. Grants +the **Governance** role set to the **Critical timelock** on **Hub_USDT**, the **Core**, **FRV** and +**Flux** yield sources, and the **HubRegistry**, so critical proposals can operate the Hub alongside the +Normal Timelock configured in the main proposal. + +#### Actions + +Grant the full Governance role set (registry, caps, queues, pause/unpause, fees, sweep, adapter updates, +\`emergencyReallocate\`, and registry \`addHub\`/\`removeHub\`) to the Critical timelock across the stack. + +#### Notes + +- The Critical timelock does not receive the operator-only \`reallocate\` role. +- Full-Governance parity with the Normal Timelock is a testnet choice; on mainnet the fast lanes would + receive only a tightening subset. Do not mirror this grant to mainnet. +- Testnet-only. The Hub is not yet deployed on any mainnet.`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + ...HUB_GOVERNANCE.map(sig => giveCallPermission(HUB_USDT, sig, CRITICAL_TIMELOCK)), + ...CORE_FLUX_GOVERNANCE.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, CRITICAL_TIMELOCK)), + ...FRV_GOVERNANCE.map(sig => giveCallPermission(FRV_SOURCE_USDT, sig, CRITICAL_TIMELOCK)), + ...CORE_FLUX_GOVERNANCE.map(sig => giveCallPermission(FLUX_SOURCE_USDT, sig, CRITICAL_TIMELOCK)), + ...HUB_REGISTRY_GOVERNANCE.map(sig => giveCallPermission(HUB_REGISTRY, sig, CRITICAL_TIMELOCK)), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip680Critical; diff --git a/vips/vip-680/bsctestnet-fast-track.ts b/vips/vip-680/bsctestnet-fast-track.ts new file mode 100644 index 000000000..9810efcb3 --- /dev/null +++ b/vips/vip-680/bsctestnet-fast-track.ts @@ -0,0 +1,74 @@ +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { + CORE_SOURCE_USDT, + FAST_TRACK_TIMELOCK, + FLUX_SOURCE_USDT, + FRV_SOURCE_USDT, + HUB_REGISTRY, + HUB_USDT, +} from "./addresses"; +import { + CORE_FLUX_GOVERNANCE, + FRV_GOVERNANCE, + HUB_GOVERNANCE, + HUB_REGISTRY_GOVERNANCE, + giveCallPermission, +} from "./permissions"; + +// --------------------------------------------------------------------------------------------------- +// VIP-680 (Fast-Track addendum) — BNB Chain Testnet. +// +// Grants the same Governance role set the main proposal gives the Normal Timelock to the Fast-Track +// timelock, across the whole stack (Hub + Core + FRV + Flux + HubRegistry), so urgent proposals can +// operate the Hub too. Split into its own proposal because all three timelocks' grants plus the wiring +// exceed a single proposal's block-gas budget on BSC. +// +// NOTE: this grants the Fast-Track timelock the FULL Governance set (fees, addYieldGroup/removeYieldGroup, +// registry management, ...) for testnet parity with the Normal Timelock. On mainnet the fast lanes would +// instead receive only a tightening subset (lower caps, pause, reorder queues); do not mirror this +// full-set grant to mainnet. +// --------------------------------------------------------------------------------------------------- + +export const vip680FastTrack = () => { + const meta = { + version: "v2", + title: "VIP-680 [BNB Chain Testnet] Liquidity Hub (USDT) — Fast-Track timelock permissions (addendum)", + description: `#### Summary + +Third of five proposals onboarding the redeployed **Liquidity Hub (USDT)** on BNB Chain Testnet. Grants +the **Governance** role set to the **Fast-Track timelock** on **Hub_USDT**, the **Core**, **FRV** and +**Flux** yield sources, and the **HubRegistry**, so fast-track proposals can operate the Hub alongside +the Normal Timelock configured in the main proposal. + +#### Actions + +Grant the full Governance role set (registry, caps, queues, pause/unpause, fees, sweep, adapter updates, +\`emergencyReallocate\`, and registry \`addHub\`/\`removeHub\`) to the Fast-Track timelock across the stack. + +#### Notes + +- The Fast-Track timelock does not receive the operator-only \`reallocate\` role. +- Full-Governance parity with the Normal Timelock is a testnet choice; on mainnet the fast lanes would + receive only a tightening subset. Do not mirror this grant to mainnet. +- Testnet-only. The Hub is not yet deployed on any mainnet.`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + ...HUB_GOVERNANCE.map(sig => giveCallPermission(HUB_USDT, sig, FAST_TRACK_TIMELOCK)), + ...CORE_FLUX_GOVERNANCE.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, FAST_TRACK_TIMELOCK)), + ...FRV_GOVERNANCE.map(sig => giveCallPermission(FRV_SOURCE_USDT, sig, FAST_TRACK_TIMELOCK)), + ...CORE_FLUX_GOVERNANCE.map(sig => giveCallPermission(FLUX_SOURCE_USDT, sig, FAST_TRACK_TIMELOCK)), + ...HUB_REGISTRY_GOVERNANCE.map(sig => giveCallPermission(HUB_REGISTRY, sig, FAST_TRACK_TIMELOCK)), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip680FastTrack; diff --git a/vips/vip-680/bsctestnet-guardian.ts b/vips/vip-680/bsctestnet-guardian.ts index dc6854f64..febba28e5 100644 --- a/vips/vip-680/bsctestnet-guardian.ts +++ b/vips/vip-680/bsctestnet-guardian.ts @@ -1,94 +1,63 @@ -import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { ProposalType } from "src/types"; import { makeProposal } from "src/utils"; +import { CORE_SOURCE_USDT, FLUX_SOURCE_USDT, FRV_SOURCE_USDT, GUARDIAN, HUB_REGISTRY, HUB_USDT } from "./addresses"; import { - CORE_SOURCE_GOVERNANCE_SIGS, - CORE_SOURCE_OPERATOR_SIGS, - CORE_SOURCE_USDT, - FLUX_SOURCE_USDT, - FRV_SOURCE_GOVERNANCE_SIGS, - FRV_SOURCE_USDT, - HUB_GOVERNANCE_SIGS, - HUB_OPERATOR_SIGS, - HUB_USDT, + CORE_FLUX_GOVERNANCE, + FRV_GOVERNANCE, + HUB_FULL, + HUB_REGISTRY_GOVERNANCE, giveCallPermission, -} from "./bsctestnet"; - -const { GUARDIAN } = NETWORK_ADDRESSES.bsctestnet; +} from "./permissions"; // --------------------------------------------------------------------------------------------------- -// VIP-680 (Guardian permissions) — TESTNET ONLY. +// VIP-680 (Guardian permissions) — BNB Chain Testnet, TESTNET ONLY. // -// The main VIP-680 proposal already grants the Guardian the Operator role set on Hub_USDT and the -// Core source. This proposal tops the Guardian up to the FULL Governance role set across the entire -// Hub stack (Hub_USDT + Core + FRV + Flux sources) so the backend can list, remove and reconfigure -// resources through the Guardian multisig during testing WITHOUT a governance proposal per change. +// Grants the Guardian multisig the FULL permission set across the whole Hub stack (Hub_USDT + Core + +// FRV + Flux sources + HubRegistry) so the backend can add, remove and reconfigure resources — list new +// FRV vaults / Flux markets, change caps, pause/unpause, swap adapters, reorder queues, register/remove +// Hubs — directly through the multisig during testing, without a governance proposal per change. // -// This is a DELIBERATE testnet-only deviation from the mainnet asymmetric permission model, where the -// Operator (Guardian) is tighten-only and only Governance (the timelocks) may loosen — add routes, -// raise caps, unpause, update adapters, sweep. Do NOT mirror this proposal to mainnet. +// This is a DELIBERATE testnet-only deviation from the mainnet asymmetric model, where the Operator +// (Guardian) is tighten-only and only Governance (the timelocks) may loosen. DO NOT mirror this proposal +// to mainnet: the mainnet Guardian must receive only the Operator sets (see permissions.ts). // -// Because the Guardian already holds the Operator (tighten) signatures from the main proposal, this -// file only grants the Governance-only DELTAS on Hub_USDT and the Core source, plus the FULL -// Governance set on the FRV and Flux sources (where the Guardian holds nothing yet). Addresses, the -// ACM target and every role string are imported from the main VIP-680 file as the single source of -// truth. Flux reuses the Core role strings (identical YieldGroup code); FRV has its own set -// (YieldGroupFRV: no raise/lowerResourceCap, no setBlocksPerYear, plus forceRemoveResource). +// The Hub grant uses HUB_FULL = Governance ∪ {reallocate} (the operator-only sig), so the Guardian holds +// every gated Hub function. On the sources the Operator sets are subsumed by Governance, so the full +// Governance set is granted directly; FRV uses its own set (YieldGroupFRV: no cap setters, no +// setBlocksPerYear, plus forceRemoveResource). // --------------------------------------------------------------------------------------------------- -// Governance-only signatures = Governance set minus the shared (also-Operator) tighten actions the -// Guardian already holds from the main proposal. -export const HUB_GOVERNANCE_ONLY_SIGS = HUB_GOVERNANCE_SIGS.filter(sig => !HUB_OPERATOR_SIGS.includes(sig)); -export const CORE_SOURCE_GOVERNANCE_ONLY_SIGS = CORE_SOURCE_GOVERNANCE_SIGS.filter( - sig => !CORE_SOURCE_OPERATOR_SIGS.includes(sig), -); - -// Flux uses the same YieldGroup contract as Core, so its Governance role set is identical. The -// Guardian holds nothing on the Flux source yet, so it receives the full set here. -export const FLUX_SOURCE_GOVERNANCE_SIGS = CORE_SOURCE_GOVERNANCE_SIGS; - export const vip680Guardian = () => { const meta = { version: "v2", - title: "VIP-680 [BNB Chain Testnet] Configure Liquidity Hub (USDT) — Guardian full permissions (testnet only)", + title: "VIP-680 [BNB Chain Testnet] Liquidity Hub (USDT) — Guardian full permissions (testnet only)", description: `#### Summary -**Testnet-only.** Tops the Guardian multisig up to the **full Governance role set** across the entire -Liquidity Hub (USDT) stack on BNB Chain Testnet: **Hub_USDT**, the **Core**, **FRV** and **Flux** -yield sources. This lets the backend add, remove and reconfigure resources (list new FRV vaults / Flux -markets, change caps, pause/unpause, swap adapters, reorder queues) directly through the Guardian +**Testnet-only.** Last of five proposals onboarding the redeployed **Liquidity Hub (USDT)** on BNB Chain +Testnet. Grants the **Guardian** multisig the **full permission set** across the entire stack: +**Hub_USDT**, the **Core**, **FRV** and **Flux** yield sources, and the **HubRegistry**. This lets the +backend add, remove and reconfigure resources (list new FRV vaults / Flux markets, change caps, +pause/unpause, swap adapters, reorder queues, register/remove Hubs) directly through the Guardian multisig during testing, without a governance proposal per change. -The main VIP-680 proposal already grants the Guardian the Operator (tighten-only) role set on -Hub_USDT and the Core source. This proposal grants the remaining Governance-only signatures on those -two contracts, plus the full Governance role set on the FRV and Flux sources (where the Guardian holds -nothing yet). - #### Access-control note -On mainnet the Hub uses an asymmetric model: only Governance (the timelocks) may loosen — add routes, -raise caps, unpause, update adapters, sweep — while the Operator (Guardian) is tighten-only. Granting -the Guardian the Governance set here is a **deliberate deviation made purely to speed up testnet -iteration, and must not be mirrored to mainnet.** +On mainnet the Hub uses an asymmetric model: only Governance (the timelocks) may loosen, while the +Operator (Guardian) is tighten-only. Granting the Guardian the full set here is a **deliberate deviation +made purely to speed up testnet iteration, and must not be mirrored to mainnet.** #### Actions Grant to the **Guardian** multisig: -1. The Governance-only signatures on **Hub_USDT** (registry, cap raises, unpause, fees, sweep, - \`emergencyReallocate\`) — completing the full Hub Governance set alongside its existing Operator - grants. -2. The Governance-only signatures on the **Core source** (\`addResource\`, \`removeResource\`, - \`updateResourceAdapter\`, \`unpauseResource\`, \`raiseResourceCap\`, \`setBlocksPerYear\`, \`sweep\`). -3. The full Governance role set on the **FRV source** (\`YieldGroupFRV\`). -4. The full Governance role set on the **Flux source** (\`YieldGroup\`, same as Core). +1. Every gated function on **Hub_USDT** — the full Governance set plus the operator-only \`reallocate\`. +2. The full Governance role set on the **Core**, **FRV** and **Flux** yield sources. +3. \`addHub\` / \`removeHub\` on the **HubRegistry**. #### Notes -- Permissions only — this proposal performs no wiring. FRV and Flux still cannot be registered until a - real FRV vault instance / Flux adapter exists on testnet; the Guardian will do that via multisig - once each resource is available. +- Permissions only — this proposal performs no wiring. - Testnet-only. The Hub is not yet deployed on any mainnet.`, forDescription: "Execute this proposal", againstDescription: "Do not execute this proposal", @@ -97,17 +66,16 @@ Grant to the **Guardian** multisig: return makeProposal( [ - // 1. Hub_USDT — Governance-only delta → Guardian (already holds the Operator/tighten set). - ...HUB_GOVERNANCE_ONLY_SIGS.map(sig => giveCallPermission(HUB_USDT, sig, GUARDIAN)), - - // 2. Core source — Governance-only delta → Guardian. - ...CORE_SOURCE_GOVERNANCE_ONLY_SIGS.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, GUARDIAN)), + // 1. Hub_USDT — every gated function (Governance ∪ reallocate) -> Guardian. + ...HUB_FULL.map(sig => giveCallPermission(HUB_USDT, sig, GUARDIAN)), - // 3. FRV source — full Governance set → Guardian (YieldGroupFRV role strings). - ...FRV_SOURCE_GOVERNANCE_SIGS.map(sig => giveCallPermission(FRV_SOURCE_USDT, sig, GUARDIAN)), + // 2. Core / FRV / Flux sources — full Governance set -> Guardian. + ...CORE_FLUX_GOVERNANCE.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, GUARDIAN)), + ...FRV_GOVERNANCE.map(sig => giveCallPermission(FRV_SOURCE_USDT, sig, GUARDIAN)), + ...CORE_FLUX_GOVERNANCE.map(sig => giveCallPermission(FLUX_SOURCE_USDT, sig, GUARDIAN)), - // 4. Flux source — full Governance set → Guardian (same YieldGroup role strings as Core). - ...FLUX_SOURCE_GOVERNANCE_SIGS.map(sig => giveCallPermission(FLUX_SOURCE_USDT, sig, GUARDIAN)), + // 3. HubRegistry — addHub / removeHub -> Guardian. + ...HUB_REGISTRY_GOVERNANCE.map(sig => giveCallPermission(HUB_REGISTRY, sig, GUARDIAN)), ], meta, ProposalType.REGULAR, diff --git a/vips/vip-680/bsctestnet-wiring.ts b/vips/vip-680/bsctestnet-wiring.ts new file mode 100644 index 000000000..c730c3b19 --- /dev/null +++ b/vips/vip-680/bsctestnet-wiring.ts @@ -0,0 +1,121 @@ +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { + ABSOLUTE_CAP_UNBOUNDED, + ADAPTER_CORE_V1, + ADAPTER_FLUX, + ADAPTER_FRV, + CORE_SOURCE_USDT, + FLUX_SOURCE_USDT, + FRV_SOURCE_USDT, + FRV_VAULT_USDT, + FUSDT_FLUX, + HUB_REGISTRY, + HUB_USDT, + PERCENTAGE_CAP_DISABLED, + VUSDT_CORE, +} from "./addresses"; + +// --------------------------------------------------------------------------------------------------- +// VIP-680 (wiring) — BNB Chain Testnet. +// +// Registers the Hub in the HubRegistry and wires all three yield sources (Core, FRV, Flux) +// end-to-end, in one atomic proposal. Depends on the main VIP-680 proposal, which grants the Normal +// Timelock the roles used here and accepts the Hub/registry ownership; this proposal must execute +// after it. +// +// Ordering is load-bearing and must not be reshuffled: +// - addHub comes FIRST, before every addYieldGroup, so HubAdded lands at a lower log index than any +// YieldGroupAdded (indexers rely on this to seed each Hub's yield-group set correctly). +// - each source's addResource precedes its inner-queue setters (the setters reject unregistered +// resources). +// - each addYieldGroup precedes the Hub's outer-queue setters (they reject unregistered groups). +// +// Outer queue order [FRV, Flux, Core] (deposit and withdraw): FRV first so deposits reach the vault +// under test; Core last because it is registered uncapped and would otherwise swallow every deposit +// before the others receive any. All three groups are registered uncapped (type(uint128).max absolute, +// 10_000 bps disabling the percentage dimension), matching the testnet policy. +// --------------------------------------------------------------------------------------------------- + +const CORE_ONLY = [VUSDT_CORE]; +const FRV_ONLY = [FRV_VAULT_USDT]; +const FLUX_ONLY = [FUSDT_FLUX]; + +// Outer queues: FRV -> Flux -> Core, same order for deposits and withdrawals. Every registered group +// is listed so the withdraw-queue funded-coverage guard is satisfied now and after any deposit. +const OUTER_QUEUE = [FRV_SOURCE_USDT, FLUX_SOURCE_USDT, CORE_SOURCE_USDT]; + +const addYieldGroup = (source: string) => ({ + target: HUB_USDT, + signature: "addYieldGroup(address,uint256,uint16)", + params: [source, ABSOLUTE_CAP_UNBOUNDED, PERCENTAGE_CAP_DISABLED], +}); + +export const vip680Wiring = () => { + const meta = { + version: "v2", + title: "VIP-680 [BNB Chain Testnet] Liquidity Hub (USDT) — register Hub and wire Core, FRV and Flux", + description: `#### Summary + +Second of five proposals onboarding the redeployed **Liquidity Hub (USDT)** on BNB Chain Testnet. +Using the Governance roles granted to the Normal Timelock in the main VIP-680 proposal, this proposal +registers the Hub in the **HubRegistry** and wires all three yield sources end-to-end so the Hub routes +deposits and withdrawals through **FRV**, **Flux** and **Core**. + +#### Actions (one atomic transaction, in order) + +1. Register **Hub_USDT** in the **HubRegistry** (\`addHub\`) — ordered before any \`addYieldGroup\` so the + \`HubAdded\` event precedes every \`YieldGroupAdded\`. +2. On the **Core** source, register **vUSDT** behind **AdapterCoreV1** and set its inner deposit/withdraw + queues. +3. On the **FRV** source, register the **FRV vault** behind **AdapterFRV** and set its inner queues. +4. On the **Flux** source, register the **Fluid fUSDT** fToken behind **AdapterFlux** and set its inner + queues. +5. Register all three sources on **Hub_USDT** with an effectively-unbounded cap, then point the Hub's + outer deposit and withdraw queues at **[FRV, Flux, Core]**. + +#### Notes + +- Queue order routes deposits to FRV first, then Flux, with Core last as the uncapped backstop. +- All three groups are registered uncapped (testnet policy); caps can be tightened later by governance + or the operator. +- Testnet-only. The Hub is not yet deployed on any mainnet.`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + // 1. Register the Hub in the registry (must precede every addYieldGroup below). + { target: HUB_REGISTRY, signature: "addHub(address)", params: [HUB_USDT] }, + + // 2. Core source: register vUSDT behind AdapterCoreV1, then set inner queues. + { target: CORE_SOURCE_USDT, signature: "addResource(address,address)", params: [VUSDT_CORE, ADAPTER_CORE_V1] }, + { target: CORE_SOURCE_USDT, signature: "setInnerDepositQueue(address[])", params: [CORE_ONLY] }, + { target: CORE_SOURCE_USDT, signature: "setInnerWithdrawQueue(address[])", params: [CORE_ONLY] }, + + // 3. FRV source: register the FRV vault behind AdapterFRV, then set inner queues. + { target: FRV_SOURCE_USDT, signature: "addResource(address,address)", params: [FRV_VAULT_USDT, ADAPTER_FRV] }, + { target: FRV_SOURCE_USDT, signature: "setInnerDepositQueue(address[])", params: [FRV_ONLY] }, + { target: FRV_SOURCE_USDT, signature: "setInnerWithdrawQueue(address[])", params: [FRV_ONLY] }, + + // 4. Flux source: register the Fluid fUSDT fToken behind AdapterFlux, then set inner queues. + { target: FLUX_SOURCE_USDT, signature: "addResource(address,address)", params: [FUSDT_FLUX, ADAPTER_FLUX] }, + { target: FLUX_SOURCE_USDT, signature: "setInnerDepositQueue(address[])", params: [FLUX_ONLY] }, + { target: FLUX_SOURCE_USDT, signature: "setInnerWithdrawQueue(address[])", params: [FLUX_ONLY] }, + + // 5. Register the three sources on the Hub (in queue order), then point the outer queues at them. + addYieldGroup(FRV_SOURCE_USDT), + addYieldGroup(FLUX_SOURCE_USDT), + addYieldGroup(CORE_SOURCE_USDT), + { target: HUB_USDT, signature: "setOuterDepositQueue(address[])", params: [OUTER_QUEUE] }, + { target: HUB_USDT, signature: "setOuterWithdrawQueue(address[])", params: [OUTER_QUEUE] }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip680Wiring; diff --git a/vips/vip-680/bsctestnet.ts b/vips/vip-680/bsctestnet.ts index 8425d15a9..fa1f7cd53 100644 --- a/vips/vip-680/bsctestnet.ts +++ b/vips/vip-680/bsctestnet.ts @@ -1,186 +1,74 @@ -import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { ProposalType } from "src/types"; import { makeProposal } from "src/utils"; -const { NORMAL_TIMELOCK, GUARDIAN, ACCESS_CONTROL_MANAGER } = NETWORK_ADDRESSES.bsctestnet; +import { + CORE_SOURCE_USDT, + FLUX_SOURCE_USDT, + FRV_SOURCE_USDT, + HUB_REGISTRY, + HUB_USDT, + NORMAL_TIMELOCK, +} from "./addresses"; +import { + CORE_FLUX_GOVERNANCE, + FRV_GOVERNANCE, + HUB_GOVERNANCE, + HUB_REGISTRY_GOVERNANCE, + giveCallPermission, +} from "./permissions"; -// AccessControlManager that the Liquidity Hub contracts were initialised with. Verified on-chain to -// equal Hub_USDT.accessControlManager() and CoreSource_USDT.accessControlManager(), and to match the -// governance-contracts testnet ACM (NETWORK_ADDRESSES.bsctestnet.ACCESS_CONTROL_MANAGER = -// 0x45f8a08F534f34A97187626E05d4b6648Eeaa9AA). Grants must target THIS ACM — it is the one the Hub -// and sources check against in _checkAccessAllowed. -export const ACM = ACCESS_CONTROL_MANAGER; - -// --------------------------------------------------------------------------------------------------- -// Liquidity Hub (USDT) — hardcoded from venus-liquidity-hub/deployments/bsctestnet/*.json. -// The Hub is not published as an @venusprotocol/*-deployments npm package, so addresses are inlined -// here rather than imported. Cross-checked against the deployment registry and on-chain state. -// --------------------------------------------------------------------------------------------------- - -// Per-asset ERC-4626 Hub (USDT) -export const HUB_USDT = "0x8C8894217b9552736CF86784B087b5114b7CfF76"; - -// Core yield source (a YieldGroup proxy over Venus Core lending) — the one source wired by this VIP. -export const CORE_SOURCE_USDT = "0x5A53efCa9ac93c6456d60E3c33839e3F06BA9356"; - -// Deferred yield sources — deployed but intentionally NOT wired by this VIP (no concrete resource on -// testnet). See the file-level note below; a follow-up VIP wires each when its resource exists. -export const FRV_SOURCE_USDT = "0x503BF2929232a0Cf7C1D296a8C59D63C6224777D"; -export const FLUX_SOURCE_USDT = "0x8Aac02EB8054F4CBAdE1396651b94F8F7D87fafc"; - -// Core resource + adapter: Venus Core-pool vUSDT and the shared stateless AdapterCoreV1. -// vUSDT is @venusprotocol/venus-protocol bsctestnet `vUSDT`; verified on-chain that -// vUSDT.underlying() == Hub_USDT.asset() (USDT) and its Comptroller.treasuryPercent() == 0 -// (required for AdapterCoreV1.validateRegistration to pass). -export const VUSDT_CORE = "0xb7526572FFE56AB9D7489838Bf2E18e3323b441A"; -export const ADAPTER_CORE_V1 = "0x480B8A3BBb6B16920c3B60cDcD64Ad059EfDb352"; - -// --------------------------------------------------------------------------------------------------- -// Caps for addYieldGroup(source, absoluteCap, percentageCapBps). // --------------------------------------------------------------------------------------------------- - -// Effectively-unbounded absolute cap. The Hub rejects type(uint256).max as InvalidCap, so -// type(uint128).max is the canonical "no ceiling" sentinel used throughout the Hub test-suite. -export const CORE_ABSOLUTE_CAP = "340282366920938463463374607431768211455"; // type(uint128).max -// 10_000 bps disables the percentage-of-TVL cap dimension, leaving only the absolute cap binding. -export const PERCENTAGE_CAP_DISABLED = 10_000; - -// --------------------------------------------------------------------------------------------------- -// ACM role strings — copied verbatim from the literals passed to _checkAccessAllowed(...) in the -// target contracts (Hub.sol / YieldGroupBase.sol / YieldGroup.sol). Split by access class per the -// Hub's asymmetric permission model (README "Permissions"): Governance can loosen AND tighten; the -// Operator can only tighten (lower caps, pause, reorder queues) plus reallocate. +// VIP-680 (main) — BNB Chain Testnet. +// +// First of five proposals that onboard the freshly redeployed Liquidity Hub (USDT) stack. This one +// gives the Normal Timelock full control of the stack: it grants the Governance role set on the Hub, +// the three yield sources (Core / FRV / Flux) and the HubRegistry, and it accepts the Hub's and the +// registry's pending Ownable2Step ownership (both were transferred to the Normal Timelock at deploy). +// +// Ownership is accepted here, early, on purpose: it retires the deployer's owner key immediately, so +// the deployer can no longer repoint `setAccessControlManager` in the deploy -> onboarding window. It +// has no effect on indexers, whose only ordering requirement (HubAdded before every YieldGroupAdded) +// lives entirely in the wiring proposal. // -// This VIP (VIP-680) provisions the Governance role set for the NORMAL timelock (the executor, which -// then performs the wiring) and the Operator role set for the Guardian. The Fast-Track and Critical -// timelocks receive the same Governance role set in the addendum proposal (bsctestnet-addendum.ts) — -// split out because all three timelocks' governance grants plus the wiring exceed the single-proposal -// block-gas budget on BSC. +// Companion proposals: a wiring proposal registers the Hub and wires the three sources end-to-end (it +// needs the roles granted here, so it executes after this one); two addendum proposals grant the same +// Governance set to the Fast-Track and Critical timelocks; and a testnet-only proposal tops the +// Guardian up to full permissions. // --------------------------------------------------------------------------------------------------- -// Hub_USDT — Governance role set (every gated Hub function except `reallocate`, which is Operator-only). -export const HUB_GOVERNANCE_SIGS = [ - "addYieldGroup(address,uint256,uint16)", - "removeYieldGroup(address)", - "raiseYieldGroupCap(address,uint256,uint16)", - "lowerYieldGroupCap(address,uint256,uint16)", - "setOuterDepositQueue(address[])", - "setOuterWithdrawQueue(address[])", - "emergencyReallocate((address,address,uint256)[],(address,address,uint256)[])", - "pauseHub()", - "unpauseHub()", - "pauseYieldGroup(address)", - "unpauseYieldGroup(address)", - "raiseMaxWithdrawalSize(uint256)", - "lowerMaxWithdrawalSize(uint256)", - "setManagementFeeBps(uint16)", - "setPerformanceFeeBps(uint16)", - "setRedeemFeeBps(uint16)", - "setFeeRecipient(address)", - "sweep(address,address)", -]; - -// Hub_USDT — Operator role set (tighten-only + reallocate + emergency pause). -export const HUB_OPERATOR_SIGS = [ - "lowerYieldGroupCap(address,uint256,uint16)", - "setOuterDepositQueue(address[])", - "setOuterWithdrawQueue(address[])", - "reallocate((address,address,uint256)[],(address,address,uint256)[])", - "pauseHub()", - "pauseYieldGroup(address)", - "lowerMaxWithdrawalSize(uint256)", -]; - -// Yield-source Governance role strings. All three sources share the YieldGroupBase gated surface; -// each subclass only adds a few functions (copied verbatim from YieldGroupBase.sol / YieldGroup.sol / -// YieldGroupFRV.sol): -// - Core & Flux use `YieldGroup` → base + raiseResourceCap / lowerResourceCap / setBlocksPerYear -// - FRV uses `YieldGroupFRV` → base + forceRemoveResource (no cap setters, no setBlocksPerYear) - -// Shared by all three sources (the 8 gated functions on YieldGroupBase). -export const YIELD_GROUP_BASE_GOVERNANCE_SIGS = [ - "addResource(address,address)", - "removeResource(address)", - "updateResourceAdapter(address,address)", - "setInnerDepositQueue(address[])", - "setInnerWithdrawQueue(address[])", - "pauseResource(address)", - "unpauseResource(address)", - "sweep(address,address)", -]; - -// CoreSource_USDT (and the Flux source) — YieldGroup: base + per-resource caps + blocksPerYear. -export const CORE_SOURCE_GOVERNANCE_SIGS = [ - ...YIELD_GROUP_BASE_GOVERNANCE_SIGS, - "raiseResourceCap(address,uint256)", - "lowerResourceCap(address,uint256)", - "setBlocksPerYear(uint256)", -]; - -// FRV_SOURCE_USDT — YieldGroupFRV: base + forceRemoveResource. It has NO raise/lowerResourceCap and -// NO setBlocksPerYear (those are Core/Flux-only), so it must not reuse CORE_SOURCE_GOVERNANCE_SIGS. -export const FRV_SOURCE_GOVERNANCE_SIGS = [...YIELD_GROUP_BASE_GOVERNANCE_SIGS, "forceRemoveResource(address)"]; - -// CoreSource_USDT — Operator role set (tighten-only). -export const CORE_SOURCE_OPERATOR_SIGS = [ - "setInnerDepositQueue(address[])", - "setInnerWithdrawQueue(address[])", - "pauseResource(address)", - "lowerResourceCap(address,uint256)", -]; - -// Operator holder: the testnet Guardian multisig (the Venus Core multisig plays the Operator role). -const OPERATOR = GUARDIAN; - -export const giveCallPermission = (contract: string, sig: string, account: string) => ({ - target: ACM, - signature: "giveCallPermission(address,string,address)", - params: [contract, sig, account], -}); - export const vip680 = () => { const meta = { version: "v2", - title: "VIP-680 [BNB Chain Testnet] Configure Liquidity Hub (USDT) — permissions and Core yield source", + title: "VIP-680 [BNB Chain Testnet] Liquidity Hub (USDT) — Normal Timelock permissions and ownership", description: `#### Summary -Post-deploy governance wiring for the newly deployed **Liquidity Hub (USDT)** on BNB Chain Testnet. -The Hub and its yield-source proxies were deployed without any ACM permissions, registry, or queue -configuration (the deploy scripts only deploy and initialise; all ACM-gated wiring is governance's -job). This VIP provisions the core access-control roles and wires the **Core** yield source -end-to-end so the USDT Hub becomes usable, routing deposits and withdrawals through Venus Core. - -Two companion proposals complete the testnet setup: an **addendum** grants the same Governance role -set to the Fast-Track and Critical timelocks, and a **Guardian-permissions** proposal tops the -Guardian up to the full Governance role set across the whole Hub stack (Hub + Core + FRV + Flux) so -backend can add and reconfigure resources via multisig without a VIP per change. +First of five proposals onboarding the newly redeployed **Liquidity Hub (USDT)** on BNB Chain Testnet. +The Hub, its three yield sources (Core, FRV, Flux) and the HubRegistry were deployed with no ACM +permissions and their ownership left pending to governance. This proposal gives the **Normal Timelock** +full control: the Governance role set across the whole stack, plus acceptance of the Hub's and +registry's pending ownership. #### Access-control model The Hub uses an asymmetric permission model: **Governance** can both loosen and tighten, while the -**Operator** can only tighten (lower caps, pause, reorder queues) plus rebalance via \`reallocate\`. - -1. Grant the **Governance** role set on **Hub_USDT** and the **Core source** to the Normal Timelock - (registry, caps, queues, pause/unpause, fees, sweep, adapter updates, \`emergencyReallocate\`). -2. Grant the **Operator** role set on **Hub_USDT** and the **Core source** to the Guardian multisig - (\`lowerYieldGroupCap\`, \`lowerMaxWithdrawalSize\`, \`lowerResourceCap\`, \`setOuter*/setInner*Queue\`, - \`pauseHub\`, \`pauseYieldGroup\`, \`pauseResource\`, \`reallocate\`). +**Operator** can only tighten (lower caps, pause, reorder queues) plus \`reallocate\`. This proposal +provisions the Governance set for the Normal Timelock, which then performs the wiring in the companion +wiring proposal. -#### Wiring +#### Actions -3. Register **vUSDT** (Venus Core pool) on the Core source behind the shared **AdapterCoreV1**, and - point the source's inner deposit/withdraw queues at it. -4. Register the Core source on **Hub_USDT** with an effectively-unbounded cap, and point the Hub's - outer deposit/withdraw queues at it. +1. Grant the **Governance** role set to the Normal Timelock on **Hub_USDT** (registry, caps, queues, + pause/unpause, fees, sweep, adapter updates, \`emergencyReallocate\`), the **Core**, **FRV** and + **Flux** yield sources, and the **HubRegistry** (\`addHub\` / \`removeHub\`). +2. Accept the pending ownership of the **HubRegistry** and **Hub_USDT** (both transferred to the Normal + Timelock at deploy), so the deployer no longer holds the owner key that gates + \`setAccessControlManager\`. #### Notes -- **FRV and Flux sources are deferred in this proposal.** Both proxies are deployed but have no - concrete resource on testnet yet: no FRV vault instance exists for USDT (only the vault - implementation and controller are deployed), and the Flux adapter is not deployed (no Fluid - LendingResolver on testnet). Once each resource exists, the Guardian multisig registers it directly - using the permissions from the companion Guardian-permissions proposal — no further VIP needed on - testnet. +- The Normal Timelock intentionally does **not** receive the operator-only \`reallocate\` role. +- No routing is wired here; the companion wiring proposal registers the Hub and the Core/FRV/Flux + resources once this proposal has executed. - Testnet-only. The Hub is not yet deployed on any mainnet.`, forDescription: "Execute this proposal", againstDescription: "Do not execute this proposal", @@ -189,33 +77,16 @@ The Hub uses an asymmetric permission model: **Governance** can both loosen and return makeProposal( [ - // 1. Governance role set (loosen + tighten) → the Normal Timelock (this proposal's executor). - ...HUB_GOVERNANCE_SIGS.map(sig => giveCallPermission(HUB_USDT, sig, NORMAL_TIMELOCK)), - ...CORE_SOURCE_GOVERNANCE_SIGS.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, NORMAL_TIMELOCK)), - - // 2. Operator role set (tighten-only + reallocate + emergency pause) → the Guardian multisig. - ...HUB_OPERATOR_SIGS.map(sig => giveCallPermission(HUB_USDT, sig, OPERATOR)), - ...CORE_SOURCE_OPERATOR_SIGS.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, OPERATOR)), - - // 3. Register vUSDT on the Core source behind AdapterCoreV1, then set its inner queues. - // addResource must precede the inner-queue setters (they reject unregistered resources). - { - target: CORE_SOURCE_USDT, - signature: "addResource(address,address)", - params: [VUSDT_CORE, ADAPTER_CORE_V1], - }, - { target: CORE_SOURCE_USDT, signature: "setInnerDepositQueue(address[])", params: [[VUSDT_CORE]] }, - { target: CORE_SOURCE_USDT, signature: "setInnerWithdrawQueue(address[])", params: [[VUSDT_CORE]] }, - - // 4. Register the Core source on the Hub, then point the outer queues at it. - // addYieldGroup must precede the outer-queue setters (they reject unregistered sources). - { - target: HUB_USDT, - signature: "addYieldGroup(address,uint256,uint16)", - params: [CORE_SOURCE_USDT, CORE_ABSOLUTE_CAP, PERCENTAGE_CAP_DISABLED], - }, - { target: HUB_USDT, signature: "setOuterDepositQueue(address[])", params: [[CORE_SOURCE_USDT]] }, - { target: HUB_USDT, signature: "setOuterWithdrawQueue(address[])", params: [[CORE_SOURCE_USDT]] }, + // 1. Governance role set -> Normal Timelock, across the whole stack. + ...HUB_GOVERNANCE.map(sig => giveCallPermission(HUB_USDT, sig, NORMAL_TIMELOCK)), + ...CORE_FLUX_GOVERNANCE.map(sig => giveCallPermission(CORE_SOURCE_USDT, sig, NORMAL_TIMELOCK)), + ...FRV_GOVERNANCE.map(sig => giveCallPermission(FRV_SOURCE_USDT, sig, NORMAL_TIMELOCK)), + ...CORE_FLUX_GOVERNANCE.map(sig => giveCallPermission(FLUX_SOURCE_USDT, sig, NORMAL_TIMELOCK)), + ...HUB_REGISTRY_GOVERNANCE.map(sig => giveCallPermission(HUB_REGISTRY, sig, NORMAL_TIMELOCK)), + + // 2. Accept pending ownership (Ownable2Step) — registry first, then the Hub. + { target: HUB_REGISTRY, signature: "acceptOwnership()", params: [] }, + { target: HUB_USDT, signature: "acceptOwnership()", params: [] }, ], meta, ProposalType.REGULAR, diff --git a/vips/vip-680/permissions.ts b/vips/vip-680/permissions.ts new file mode 100644 index 000000000..9fcd77b0d --- /dev/null +++ b/vips/vip-680/permissions.ts @@ -0,0 +1,124 @@ +import { ACM } from "./addresses"; + +// =================================================================================================== +// VIP-680 — Liquidity Hub (USDT) ACM role-string sets. +// +// Every string is the literal function signature passed to `_checkAccessAllowed(...)` in the deployed +// contracts (the ACM role is `keccak256(targetContract, roleString)`), copied verbatim from +// venus-liquidity-hub/contracts. The asymmetric model (README "Permissions"): Governance may loosen +// AND tighten; the Operator may only tighten (lower caps, pause, reorder queues) plus `reallocate`. +// +// Source of truth per contract: +// Hub/Hub.sol -> HUB_* (19 gated fns: 18 governance + operator-only reallocate) +// YieldGroup/base/YieldGroupBase -> YIELD_GROUP_BASE (8 shared gated fns) +// YieldGroup/YieldGroup.sol -> CORE_FLUX_* (base + resource caps + setBlocksPerYear) +// YieldGroup/YieldGroupFRV.sol -> FRV_* (base + forceRemoveResource; NO caps, NO blocksPerYear) +// registry/HubRegistry.sol -> HUB_REGISTRY (addHub / removeHub) +// =================================================================================================== + +// Operator-only on the Hub: net-zero rebalance, callable only while unpaused. NOT in the Governance +// set (governance holds the pause-bypassing `emergencyReallocate` instead). +export const REALLOCATE = "reallocate((address,address,uint256)[],(address,address,uint256)[])"; + +// --------------------------------------------------------------------------------------------------- +// Hub_USDT. +// --------------------------------------------------------------------------------------------------- + +// Governance role set: every gated Hub function EXCEPT `reallocate` (operator-only). +export const HUB_GOVERNANCE = [ + "addYieldGroup(address,uint256,uint16)", + "removeYieldGroup(address)", + "raiseYieldGroupCap(address,uint256,uint16)", + "lowerYieldGroupCap(address,uint256,uint16)", + "setOuterDepositQueue(address[])", + "setOuterWithdrawQueue(address[])", + "emergencyReallocate((address,address,uint256)[],(address,address,uint256)[])", + "pauseHub()", + "unpauseHub()", + "pauseYieldGroup(address)", + "unpauseYieldGroup(address)", + "raiseMaxWithdrawalSize(uint256)", + "lowerMaxWithdrawalSize(uint256)", + "setManagementFeeBps(uint16)", + "setPerformanceFeeBps(uint16)", + "setRedeemFeeBps(uint16)", + "setFeeRecipient(address)", + "sweep(address,address)", +]; + +// Operator role set: tighten-only + `reallocate` + emergency pause. +export const HUB_OPERATOR = [ + "lowerYieldGroupCap(address,uint256,uint16)", + "setOuterDepositQueue(address[])", + "setOuterWithdrawQueue(address[])", + REALLOCATE, + "pauseHub()", + "pauseYieldGroup(address)", + "lowerMaxWithdrawalSize(uint256)", +]; + +// Full control on the Hub = Governance ∪ {reallocate}, deduped (reallocate is the only operator sig +// not already in HUB_GOVERNANCE). Used only by the testnet Guardian proposal. +export const HUB_FULL = [...HUB_GOVERNANCE, REALLOCATE]; + +// --------------------------------------------------------------------------------------------------- +// Yield sources (Core / FRV / Flux). All three share the YieldGroupBase surface; each subclass adds +// a few functions. +// --------------------------------------------------------------------------------------------------- + +// Shared by all three sources. +export const YIELD_GROUP_BASE = [ + "addResource(address,address)", + "removeResource(address)", + "updateResourceAdapter(address,address)", + "setInnerDepositQueue(address[])", + "setInnerWithdrawQueue(address[])", + "pauseResource(address)", + "unpauseResource(address)", + "sweep(address,address)", +]; + +// Core & Flux use `YieldGroup`: base + per-resource caps + blocksPerYear. +export const CORE_FLUX_GOVERNANCE = [ + ...YIELD_GROUP_BASE, + "raiseResourceCap(address,uint256)", + "lowerResourceCap(address,uint256)", + "setBlocksPerYear(uint256)", +]; + +// FRV uses `YieldGroupFRV`: base + forceRemoveResource. It has NO cap setters and NO setBlocksPerYear, +// so it must not reuse CORE_FLUX_GOVERNANCE. +export const FRV_GOVERNANCE = [...YIELD_GROUP_BASE, "forceRemoveResource(address)"]; + +// Operator (tighten-only) sets. Core/Flux can lower per-resource caps; FRV has no cap to lower. +export const CORE_FLUX_OPERATOR = [ + "setInnerDepositQueue(address[])", + "setInnerWithdrawQueue(address[])", + "pauseResource(address)", + "lowerResourceCap(address,uint256)", +]; +export const FRV_OPERATOR = [ + "setInnerDepositQueue(address[])", + "setInnerWithdrawQueue(address[])", + "pauseResource(address)", +]; + +// --------------------------------------------------------------------------------------------------- +// HubRegistry — the only two gated functions. +// --------------------------------------------------------------------------------------------------- +export const HUB_REGISTRY_GOVERNANCE = ["addHub(address)", "removeHub(address)"]; + +// --------------------------------------------------------------------------------------------------- +// Command builder: ACM.giveCallPermission(contract, roleString, account). ACM is the single ACM the +// Hub stack checks against (see addresses.ts). +// --------------------------------------------------------------------------------------------------- +export const giveCallPermission = (contract: string, sig: string, account: string) => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [contract, sig, account], +}); + +// NOTE for a future mainnet VIP: the mainnet Guardian must receive ONLY the Operator sets +// (HUB_OPERATOR on the Hub, CORE_FLUX_OPERATOR on Core/Flux, FRV_OPERATOR on FRV — no registry, no +// Governance). HUB_FULL / the full-Governance Guardian grant in bsctestnet-guardian.ts is a +// testnet-only deviation and must not be mirrored to mainnet.