diff --git a/scripts/fetchCoreMarketCaps.ts b/scripts/fetchCoreMarketCaps.ts new file mode 100644 index 000000000..8f7c8bed3 --- /dev/null +++ b/scripts/fetchCoreMarketCaps.ts @@ -0,0 +1,175 @@ +/** + * Build the Executor's per-market 20% floor table for VIP-701. + * + * Pipeline (per vToken returned by Unitroller.getAllMarkets()): + * 1. Skip if not listed in the core pool (poolMarkets(0, vToken).isListed === false). + * 2. Pick the effective caps: + * - If the market is touched by queued VIP-622 (marketCapChanges or delistAssets in + * vips/vip-622/data/bscmainnet.ts), use the post-VIP-622 caps from that file. + * - Otherwise read the live caps from Comptroller.borrowCaps / supplyCaps. + * 3. Compute 20% floors. Integer division means a 1-wei dust cap rounds to 0. + * 4. Skip the market entirely if both 20% floors are zero — covers fully delisted + * markets (THE/TUSD/FIL via VIP-622) and dormant zero-cap stubs + * (BUSD/SXP/MATIC/TUSDOLD/TRXOLD/vBETH). + * + * Run: + * npx hardhat run scripts/fetchCoreMarketCaps.ts --network bscmainnet + * + * Output: + * vips/vip-701/coreMarketCaps.json — consumed directly by vips/vip-701/bscmainnet.ts. + */ +import { BigNumber } from "ethers"; +import fs from "fs"; +import { ethers } from "hardhat"; +import path from "path"; + +import { delistAssets, marketCapChanges } from "../vips/vip-622/data/bscmainnet"; + +const UNITROLLER = "0xfD36E2c2a6789Db23113685031d7F16329158384"; +const CORE_POOL_ID = 0; +const FLOOR_PERCENT = 20; + +const COMPTROLLER_ABI = [ + "function getAllMarkets() view returns (address[])", + "function poolMarkets(uint96,address) view returns (bool isListed,uint256,bool,uint256,uint256,uint96,bool)", + "function borrowCaps(address) view returns (uint256)", + "function supplyCaps(address) view returns (uint256)", +]; +const VTOKEN_ABI = ["function symbol() view returns (string)"]; + +interface PostVip622Cap { + symbol: string; + borrowCap: BigNumber; + supplyCap: BigNumber; +} + +interface KeptMarket { + address: string; + symbol: string; + capSource: "live" | "vip-622"; + effectiveBorrowCap: string; + effectiveSupplyCap: string; + minBorrowCap: string; + minSupplyCap: string; +} +interface SkippedMarket { + address: string; + symbol?: string; + reason: string; +} + +// Build the VIP-622 override map directly from PR #706's data file. +// Single source of truth: any future edit to vips/vip-622/data/bscmainnet.ts flows here. +function buildVip622Overrides(): Map { + const overrides = new Map(); + for (const m of marketCapChanges) { + overrides.set(m.vToken.toLowerCase(), { + symbol: m.symbol, + borrowCap: BigNumber.from(m.borrowCap.new), + supplyCap: BigNumber.from(m.supplyCap.new), + }); + } + for (const d of delistAssets) { + // delistAssets force caps to 0; marketCapChanges may also list the same vToken (no-op), + // but the delist intent wins so we overwrite unconditionally. + overrides.set(d.vToken.toLowerCase(), { + symbol: d.symbol, + borrowCap: BigNumber.from(0), + supplyCap: BigNumber.from(0), + }); + } + return overrides; +} + +async function main() { + const provider = ethers.provider; + const comptroller = new ethers.Contract(UNITROLLER, COMPTROLLER_ABI, provider); + const block = await provider.getBlockNumber(); + console.log(`Fetching at block ${block}…`); + + const vip622Overrides = buildVip622Overrides(); + console.log(`VIP-622 supplies post-caps for ${vip622Overrides.size} markets`); + + const allMarkets: string[] = await comptroller.getAllMarkets(); + console.log(`getAllMarkets() returned ${allMarkets.length} markets`); + + const kept: KeptMarket[] = []; + const skipped: SkippedMarket[] = []; + + for (const market of allMarkets) { + const [isListed] = await comptroller.poolMarkets(CORE_POOL_ID, market); + if (!isListed) { + skipped.push({ address: market, reason: "not listed in core pool (poolId 0)" }); + continue; + } + + const override = vip622Overrides.get(market.toLowerCase()); + let borrowCap: BigNumber; + let supplyCap: BigNumber; + let capSource: "live" | "vip-622"; + if (override) { + borrowCap = override.borrowCap; + supplyCap = override.supplyCap; + capSource = "vip-622"; + } else { + [borrowCap, supplyCap] = await Promise.all([comptroller.borrowCaps(market), comptroller.supplyCaps(market)]); + capSource = "live"; + } + + const minBorrowCap = borrowCap.mul(FLOOR_PERCENT).div(100); + const minSupplyCap = supplyCap.mul(FLOOR_PERCENT).div(100); + + const symbol = await new ethers.Contract(market, VTOKEN_ABI, provider).symbol(); + + if (minBorrowCap.isZero() && minSupplyCap.isZero()) { + skipped.push({ + address: market, + symbol, + reason: `${capSource} 20% floors are both zero (delisted / dormant)`, + }); + continue; + } + + kept.push({ + address: market, + symbol, + capSource, + effectiveBorrowCap: borrowCap.toString(), + effectiveSupplyCap: supplyCap.toString(), + minBorrowCap: minBorrowCap.toString(), + minSupplyCap: minSupplyCap.toString(), + }); + } + + console.log(`Kept ${kept.length} markets, skipped ${skipped.length}`); + console.log("Skipped:", skipped); + + const out = { + _meta: { + network: "bscmainnet", + comptroller: UNITROLLER, + corePoolId: CORE_POOL_ID, + block, + fetchedAt: new Date().toISOString(), + floorPercent: FLOOR_PERCENT, + vip622DataSource: "vips/vip-622/data/bscmainnet.ts (PR https://github.com/VenusProtocol/vips/pull/706)", + }, + markets: kept, + skipped, + }; + + const outPath = path.resolve(__dirname, "../vips/vip-701/coreMarketCaps.json"); + fs.writeFileSync(outPath, `${JSON.stringify(out, null, 2)}\n`); + console.log(`Wrote ${outPath}`); + + console.log("\n--- Per-market 20% floors ---"); + console.log("symbol".padEnd(28), "source".padEnd(10), "minBorrowCap".padEnd(28), "minSupplyCap"); + for (const m of kept) { + console.log(m.symbol.padEnd(28), m.capSource.padEnd(10), m.minBorrowCap.padEnd(28), m.minSupplyCap); + } +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/simulations/vip-623/abi/AccessControlManager.json b/simulations/vip-623/abi/AccessControlManager.json new file mode 100644 index 000000000..4a118fcc4 --- /dev/null +++ b/simulations/vip-623/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-623/abi/Comptroller.json b/simulations/vip-623/abi/Comptroller.json new file mode 100644 index 000000000..f003a4837 --- /dev/null +++ b/simulations/vip-623/abi/Comptroller.json @@ -0,0 +1,31 @@ +[ + { + "inputs": [{ "internalType": "address", "name": "vToken", "type": "address" }], + "name": "markets", + "outputs": [ + { "internalType": "bool", "name": "isListed", "type": "bool" }, + { "internalType": "uint256", "name": "collateralFactorMantissa", "type": "uint256" }, + { "internalType": "bool", "name": "isVenus", "type": "bool" }, + { "internalType": "uint256", "name": "liquidationThresholdMantissa", "type": "uint256" }, + { "internalType": "uint256", "name": "liquidationIncentiveMantissa", "type": "uint256" }, + { "internalType": "uint96", "name": "marketPoolId", "type": "uint96" }, + { "internalType": "bool", "name": "isBorrowAllowed", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "borrowCaps", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "supplyCaps", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-623/abi/EBrake.json b/simulations/vip-623/abi/EBrake.json new file mode 100644 index 000000000..c7aedf09d --- /dev/null +++ b/simulations/vip-623/abi/EBrake.json @@ -0,0 +1,683 @@ +[ + { + "inputs": [ + { + "internalType": "contract ICorePoolComptroller", + "name": "corePoolComptroller_", + "type": "address" + }, + { + "internalType": "bool", + "name": "isIsolatedPool_", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "ArrayLengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint256", + "name": "currentCap", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requestedCap", + "type": "uint256" + } + ], + "name": "CapExceedsCurrent", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyArray", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum IComptroller.Action", + "name": "action", + "type": "uint8" + } + ], + "name": "ForbiddenAction", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "MarketNotListed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorCode", + "type": "uint256" + } + ], + "name": "SetCollateralFactorFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum IComptroller.Action", + "name": "action", + "type": "uint8" + } + ], + "name": "ActionPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "enum IComptroller.Action[]", + "name": "actions", + "type": "uint8[]" + } + ], + "name": "ActionsPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "newBorrowCaps", + "type": "uint256[]" + } + ], + "name": "BorrowCapsDecreased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "CollateralFactorZeroed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + } + ], + "name": "FlashLoanPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "MarketStateReset", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAccessControlManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAccessControlManager", + "type": "address" + } + ], + "name": "NewAccessControlManager", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "newSupplyCaps", + "type": "uint256[]" + } + ], + "name": "SupplyCapsDecreased", + "type": "event" + }, + { + "inputs": [], + "name": "COMPTROLLER", + "outputs": [ + { + "internalType": "contract ICorePoolComptroller", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "IS_ISOLATED_POOL", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "getMarketCFSnapshot", + "outputs": [ + { + "internalType": "uint256", + "name": "cf", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lt", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "marketStates", + "outputs": [ + { + "internalType": "uint256", + "name": "borrowCap", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "supplyCap", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "borrowCapSnapshotted", + "type": "bool" + }, + { + "internalType": "bool", + "name": "supplyCapSnapshotted", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "internalType": "enum IComptroller.Action[]", + "name": "actions", + "type": "uint8[]" + } + ], + "name": "pauseActions", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "pauseBorrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pauseFlashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "pauseRedeem", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "pauseSupply", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "pauseTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "resetMarketState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "setAccessControlManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "setCFZero", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "setCFZero", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newBorrowCaps", + "type": "uint256[]" + } + ], + "name": "setMarketBorrowCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newSupplyCaps", + "type": "uint256[]" + } + ], + "name": "setMarketSupplyCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-623/abi/ERC20.json b/simulations/vip-623/abi/ERC20.json new file mode 100644 index 000000000..3a509c9c4 --- /dev/null +++ b/simulations/vip-623/abi/ERC20.json @@ -0,0 +1,134 @@ +[ + { + "inputs": [ + { "internalType": "string", "name": "name_", "type": "string" }, + { "internalType": "string", "name": "symbol_", "type": "string" }, + { "internalType": "uint8", "name": "decimals_", "type": "uint8" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], + "name": "faucet", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-623/abi/Executor.json b/simulations/vip-623/abi/Executor.json new file mode 100644 index 000000000..4a4cf23f0 --- /dev/null +++ b/simulations/vip-623/abi/Executor.json @@ -0,0 +1,267 @@ +[ + { + "inputs": [ + { "internalType": "contract IEBrake", "name": "eBrake_", "type": "address" }, + { "internalType": "contract ICorePoolComptroller", "name": "comptroller_", "type": "address" }, + { "internalType": "bool", "name": "isCorePool_", "type": "bool" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "adjustedCap", "type": "uint256" }, + { "internalType": "uint256", "name": "minCap", "type": "uint256" } + ], + "name": "CapBelowMinimum", + "type": "error" + }, + { "inputs": [], "name": "CapNotBreached", "type": "error" }, + { + "inputs": [{ "internalType": "address", "name": "market", "type": "address" }], + "name": "MarketDisabled", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "market", "type": "address" }], + "name": "MarketNotConfigured", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "market", "type": "address" }], + "name": "MarketNotListed", + "type": "error" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "calledContract", "type": "address" }, + { "internalType": "string", "name": "methodSignature", "type": "string" } + ], + "name": "Unauthorized", + "type": "error" + }, + { "inputs": [], "name": "ZeroAddress", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "caller", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "market", "type": "address" } + ], + "name": "BorrowCapExceeding", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "caller", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "market", "type": "address" }, + { "indexed": false, "internalType": "enum IExecutor.CapType", "name": "capType", "type": "uint8" }, + { "indexed": false, "internalType": "uint256", "name": "oldCap", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "newCap", "type": "uint256" } + ], + "name": "CapAdjusted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "uint8", "name": "version", "type": "uint8" }], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "caller", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "market", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "newLTV", "type": "uint256" } + ], + "name": "LTVAdjusted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "market", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "minBorrowCap", "type": "uint256" }, + { "internalType": "uint256", "name": "minSupplyCap", "type": "uint256" }, + { "internalType": "bool", "name": "enabled", "type": "bool" } + ], + "indexed": false, + "internalType": "struct IExecutor.MarketConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "MarketConfigSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "oldAccessControlManager", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "newAccessControlManager", "type": "address" } + ], + "name": "NewAccessControlManager", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "caller", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "market", "type": "address" } + ], + "name": "SupplyCapExceeding", + "type": "event" + }, + { + "inputs": [], + "name": "COMPTROLLER", + "outputs": [{ "internalType": "contract ICorePoolComptroller", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EBRAKE", + "outputs": [{ "internalType": "contract IEBrake", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "IS_CORE_POOL", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "acceptOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [{ "internalType": "contract IAccessControlManagerV8", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "market", "type": "address" }], + "name": "handleBorrowCapExceeding", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "enum IExecutor.CapType", "name": "capType", "type": "uint8" }, + { "internalType": "uint256", "name": "adjustedCap", "type": "uint256" } + ], + "name": "handleCapAdjust", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "market", "type": "address" }, + { "internalType": "uint256", "name": "adjustedLTV", "type": "uint256" } + ], + "name": "handleLTVAdjust", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "market", "type": "address" }], + "name": "handleSupplyCapExceeding", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "accessControlManager_", "type": "address" }], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "marketConfigs", + "outputs": [ + { "internalType": "uint256", "name": "minBorrowCap", "type": "uint256" }, + { "internalType": "uint256", "name": "minSupplyCap", "type": "uint256" }, + { "internalType": "bool", "name": "enabled", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [{ "internalType": "address", "name": "accessControlManager_", "type": "address" }], + "name": "setAccessControlManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "market", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "minBorrowCap", "type": "uint256" }, + { "internalType": "uint256", "name": "minSupplyCap", "type": "uint256" }, + { "internalType": "bool", "name": "enabled", "type": "bool" } + ], + "internalType": "struct IExecutor.MarketConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "setMarketConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-623/bscmainnet.ts b/simulations/vip-623/bscmainnet.ts new file mode 100644 index 000000000..883e39ab3 --- /dev/null +++ b/simulations/vip-623/bscmainnet.ts @@ -0,0 +1,419 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { BigNumber, Contract } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { expectEvents, initMainnetUser } from "src/utils"; +import { forking, pretendExecutingVip, testVip } from "src/vip-framework"; + +import { vip622 } from "../../vips/vip-622/bscmainnet"; +import vip623, { + ACM, + CORE_POOL_MARKET_CONFIGS, + EBRAKE, + EBRAKE_EXECUTOR_PERMS, + EXECUTOR, + EXECUTOR_GOVERNANCE_PERMS, + EXECUTOR_MONITOR_PERMS, + FLUX_MARKETING_WALLET, + SIGNAL_MONITOR, + USDT, + USDT_AMOUNT, +} from "../../vips/vip-623/bscmainnet"; +import coreMarketCaps from "../../vips/vip-623/coreMarketCaps.json"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; +import COMPTROLLER_ABI from "./abi/Comptroller.json"; +import EBRAKE_ABI from "./abi/EBrake.json"; +import ERC20_ABI from "./abi/ERC20.json"; +import EXECUTOR_ABI from "./abi/Executor.json"; + +const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN, UNITROLLER, VTREASURY } = + NETWORK_ADDRESSES.bscmainnet; +const EXECUTOR_GOVERNANCE_ACCOUNTS = [GUARDIAN, NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; + +// Listed Core Pool market driving the happy-path round-trip. +const VUSDC = "0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8"; +// Unlisted address used to exercise MarketNotConfigured (every Core Pool market is configured by the VIP). +const UNLISTED_MARKET = "0x000000000000000000000000000000000000bEEF"; + +// IExecutor.CapType wire values — load-bearing. +const CAP_TYPE_BORROW = 0; +const CAP_TYPE_SUPPLY = 1; + +// Block after Executor is deployed on BSC mainnet. +const BLOCK_NUMBER = 98248415; + +forking(BLOCK_NUMBER, async () => { + let accessControlManager: Contract; + let executor: Contract; + let eBrake: Contract; + let comptroller: Contract; + let usdt: Contract; + + // BSC mainnet ACM's isAllowedToCall keys on msg.sender == target contract. + // We impersonate the relevant target contract for each permission lookup. + let impersonatedExecutor: SignerWithAddress; + let impersonatedEBrake: SignerWithAddress; + + let monitorSigner: SignerWithAddress; + let criticalTimelockSigner: SignerWithAddress; + let randomSigner: SignerWithAddress; + + // Captured before VIP execution so post-VIP assertions can verify the delta. + let treasuryUsdtBefore: BigNumber; + let fluxWalletUsdtBefore: BigNumber; + + before(async () => { + accessControlManager = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACM); + executor = await ethers.getContractAt(EXECUTOR_ABI, EXECUTOR); + eBrake = await ethers.getContractAt(EBRAKE_ABI, EBRAKE); + comptroller = await ethers.getContractAt(COMPTROLLER_ABI, UNITROLLER); + usdt = await ethers.getContractAt(ERC20_ABI, USDT); + + impersonatedExecutor = await initMainnetUser(EXECUTOR, ethers.utils.parseEther("1")); + impersonatedEBrake = await initMainnetUser(EBRAKE, ethers.utils.parseEther("1")); + + monitorSigner = await initMainnetUser(SIGNAL_MONITOR, ethers.utils.parseEther("1")); + criticalTimelockSigner = await initMainnetUser(CRITICAL_TIMELOCK, ethers.utils.parseEther("1")); + randomSigner = await initMainnetUser("0x000000000000000000000000000000000000bEEF", ethers.utils.parseEther("1")); + + // VIP-622 is queued at this fork block. coreMarketCaps.json was built off the same + // VIP-622 data file, so pre-executing it here makes on-chain caps match the snapshot + // the VIP-623 floors were computed from — the post-VIP-623 assertion below can then + // verify each stored floor equals 20% of the comptroller's live cap. + await pretendExecutingVip(await vip622(), NORMAL_TIMELOCK); + + treasuryUsdtBefore = await usdt.balanceOf(VTREASURY); + fluxWalletUsdtBefore = await usdt.balanceOf(FLUX_MARKETING_WALLET); + }); + + describe("Pre-VIP behavior", () => { + it("Signal monitor should not yet have Executor action permissions", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const sig of EXECUTOR_MONITOR_PERMS) { + expect(await acm.isAllowedToCall(SIGNAL_MONITOR, sig)).to.equal(false, `unexpected permission: ${sig}`); + } + }); + + it("Executor should not yet have EBrake permissions", async () => { + const acm = accessControlManager.connect(impersonatedEBrake); + for (const sig of EBRAKE_EXECUTOR_PERMS) { + expect(await acm.isAllowedToCall(EXECUTOR, sig)).to.equal(false, `unexpected permission: ${sig}`); + } + }); + + it("Guardian and Timelocks should not yet have setMarketConfig permission on Executor", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const account of EXECUTOR_GOVERNANCE_ACCOUNTS) { + for (const sig of EXECUTOR_GOVERNANCE_PERMS) { + expect(await acm.isAllowedToCall(account, sig)).to.equal( + false, + `unexpected permission ${sig} for ${account}`, + ); + } + } + }); + + it("Signal monitor cannot call Executor handlers before the VIP runs", async () => { + await expect(executor.connect(monitorSigner).handleLTVAdjust(VUSDC, 0)).to.be.reverted; + }); + + it("Executor and EBrake are not yet owned by Normal Timelock (pendingOwner is set)", async () => { + expect(await executor.owner()).to.not.equal(NORMAL_TIMELOCK); + expect(await executor.pendingOwner()).to.equal(NORMAL_TIMELOCK); + expect(await eBrake.owner()).to.not.equal(NORMAL_TIMELOCK); + expect(await eBrake.pendingOwner()).to.equal(NORMAL_TIMELOCK); + }); + }); + + testVip("VIP-623 [BNB Chain] EBrake Executor Phase -1 Activation & Flux Campaign Funding", await vip623(), { + callbackAfterExecution: async txResponse => { + // 13 RoleGranted, 2 OwnershipTransferred (Executor + EBrake), 1 MarketConfigSet per Core Pool market. + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [13]); + await expectEvents(txResponse, [EXECUTOR_ABI], ["OwnershipTransferred"], [2]); + await expectEvents(txResponse, [EXECUTOR_ABI], ["MarketConfigSet"], [CORE_POOL_MARKET_CONFIGS.length]); + }, + }); + + describe("Post-VIP permission state", () => { + it("Signal monitor should have all Executor action permissions", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const sig of EXECUTOR_MONITOR_PERMS) { + expect(await acm.isAllowedToCall(SIGNAL_MONITOR, sig)).to.equal(true, `missing permission: ${sig}`); + } + }); + + it("Executor should have all EBrake permissions", async () => { + const acm = accessControlManager.connect(impersonatedEBrake); + for (const sig of EBRAKE_EXECUTOR_PERMS) { + expect(await acm.isAllowedToCall(EXECUTOR, sig)).to.equal(true, `missing permission: ${sig}`); + } + }); + + it("Guardian and Timelocks should have setMarketConfig permission on Executor", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const account of EXECUTOR_GOVERNANCE_ACCOUNTS) { + for (const sig of EXECUTOR_GOVERNANCE_PERMS) { + expect(await acm.isAllowedToCall(account, sig)).to.equal(true, `missing permission ${sig} for ${account}`); + } + } + }); + }); + + describe("Executor deployment linkage", () => { + it("Executor.EBRAKE should equal the configured EBrake address", async () => { + expect(await executor.EBRAKE()).to.equal(EBRAKE); + }); + + it("Executor.COMPTROLLER should equal the Core Pool Unitroller", async () => { + expect(await executor.COMPTROLLER()).to.equal(UNITROLLER); + }); + + it("Executor.IS_CORE_POOL should be true (BSC Diamond comptroller)", async () => { + expect(await executor.IS_CORE_POOL()).to.equal(true); + }); + }); + + describe("Post-VIP ownership state", () => { + it("Executor.owner() is Normal Timelock and pendingOwner is cleared", async () => { + expect(await executor.owner()).to.equal(NORMAL_TIMELOCK); + expect(await executor.pendingOwner()).to.equal(ethers.constants.AddressZero); + }); + + it("EBrake.owner() is Normal Timelock and pendingOwner is cleared", async () => { + expect(await eBrake.owner()).to.equal(NORMAL_TIMELOCK); + expect(await eBrake.pendingOwner()).to.equal(ethers.constants.AddressZero); + }); + }); + + describe("Post-VIP Core Pool market configs", () => { + // With VIP-622 pre-executed in `before`, the comptroller's borrow/supply caps now reflect + // the post-VIP-622 state. The script computed VIP-623's floors off the same VIP-622 data, + // so the Executor's stored floor for every configured market must equal 20% of the live cap + // and the live cap must equal the snapshot the script saw. Catches drift between the two. + it("every Core Pool market's stored floors equal 20% of the post-VIP-622 on-chain caps", async () => { + for (const m of CORE_POOL_MARKET_CONFIGS) { + const [liveBorrowCap, liveSupplyCap, cfg] = await Promise.all([ + comptroller.borrowCaps(m.address), + comptroller.supplyCaps(m.address), + executor.marketConfigs(m.address), + ]); + const expectedMinBorrow = liveBorrowCap.mul(20).div(100); + const expectedMinSupply = liveSupplyCap.mul(20).div(100); + + expect(cfg.minBorrowCap).to.equal( + expectedMinBorrow, + `minBorrowCap for ${m.symbol} (${ + m.address + }): stored ${cfg.minBorrowCap.toString()} vs 20% of live ${liveBorrowCap.toString()}`, + ); + expect(cfg.minSupplyCap).to.equal( + expectedMinSupply, + `minSupplyCap for ${m.symbol} (${ + m.address + }): stored ${cfg.minSupplyCap.toString()} vs 20% of live ${liveSupplyCap.toString()}`, + ); + expect(cfg.enabled).to.equal(true, `enabled for ${m.symbol} (${m.address})`); + } + }); + }); + + describe("Skipped markets are skipped for the right reason", () => { + for (const s of coreMarketCaps.skipped) { + const label = `${s.symbol ?? "(unknown symbol)"} ${s.address}`; + if (s.reason.includes("not listed in core pool")) { + it(`${label}: isListed === false in core pool`, async () => { + const [isListed] = await comptroller.markets(s.address); + expect(isListed).to.equal(false); + }); + } else if (s.reason.includes("20% floors are both zero")) { + // VIP-622 has been pre-executed, so live caps already reflect the effective state + // the script used. Both 20% values must round to zero — otherwise the script + // dropped a market that should have stayed in. + it(`${label}: 20% of live borrow & supply caps both round to zero`, async () => { + const [liveBorrow, liveSupply] = await Promise.all([ + comptroller.borrowCaps(s.address), + comptroller.supplyCaps(s.address), + ]); + expect(liveBorrow.mul(20).div(100)).to.equal( + 0, + `unexpected non-zero 20% borrow floor (cap ${liveBorrow.toString()})`, + ); + expect(liveSupply.mul(20).div(100)).to.equal( + 0, + `unexpected non-zero 20% supply floor (cap ${liveSupply.toString()})`, + ); + }); + } else { + it(`${label}: unknown skip reason "${s.reason}"`, () => { + throw new Error(`Unhandled skip reason: ${s.reason}. Update the test or the script.`); + }); + } + } + + it("Executor was NOT configured for any skipped market", async () => { + for (const s of coreMarketCaps.skipped) { + const cfg = await executor.marketConfigs(s.address); + expect(cfg.enabled).to.equal(false, `${s.symbol ?? s.address} should be unconfigured`); + expect(cfg.minBorrowCap).to.equal(0, `${s.symbol ?? s.address} should have zero stored borrow floor`); + expect(cfg.minSupplyCap).to.equal(0, `${s.symbol ?? s.address} should have zero stored supply floor`); + } + }); + }); + + describe("Post-VIP behaviour — setMarketConfig", () => { + it("reverts when called from an unauthorized account", async () => { + await expect( + executor.connect(randomSigner).setMarketConfig(VUSDC, { minBorrowCap: 0, minSupplyCap: 0, enabled: true }), + ).to.be.reverted; + }); + + it("reverts with ZeroAddress when market is the zero address", async () => { + await expect( + executor + .connect(criticalTimelockSigner) + .setMarketConfig(ethers.constants.AddressZero, { minBorrowCap: 0, minSupplyCap: 0, enabled: true }), + ).to.be.revertedWithCustomError(executor, "ZeroAddress"); + }); + + it("reverts when market is not listed in the comptroller", async () => { + const unlistedMarket = "0x000000000000000000000000000000000000DEAD"; + await expect( + executor + .connect(criticalTimelockSigner) + .setMarketConfig(unlistedMarket, { minBorrowCap: 0, minSupplyCap: 0, enabled: true }), + ).to.be.reverted; + }); + + it("Critical timelock can configure a listed market and the config is stored", async () => { + const currentBorrowCap: BigNumber = await comptroller.borrowCaps(VUSDC); + const currentSupplyCap: BigNumber = await comptroller.supplyCaps(VUSDC); + // Floor at half the live caps — high enough to actually reject below-floor adjustments, + // low enough that the tightening round-trip further down still has room to move. + const minBorrowCap = currentBorrowCap.div(2); + const minSupplyCap = currentSupplyCap.div(2); + + await expect( + executor.connect(criticalTimelockSigner).setMarketConfig(VUSDC, { minBorrowCap, minSupplyCap, enabled: true }), + ) + .to.emit(executor, "MarketConfigSet") + .withArgs(VUSDC, [minBorrowCap, minSupplyCap, true]); + + const stored = await executor.marketConfigs(VUSDC); + expect(stored.minBorrowCap).to.equal(minBorrowCap); + expect(stored.minSupplyCap).to.equal(minSupplyCap); + expect(stored.enabled).to.equal(true); + }); + }); + + describe("Post-VIP behaviour — handler access control", () => { + it("non-monitor caller cannot invoke handleLTVAdjust", async () => { + await expect(executor.connect(randomSigner).handleLTVAdjust(VUSDC, 0)).to.be.reverted; + }); + + it("non-monitor caller cannot invoke handleCapAdjust", async () => { + await expect(executor.connect(randomSigner).handleCapAdjust(VUSDC, CAP_TYPE_BORROW, 0)).to.be.reverted; + }); + + it("non-monitor caller cannot invoke handleSupplyCapExceeding", async () => { + await expect(executor.connect(randomSigner).handleSupplyCapExceeding(VUSDC)).to.be.reverted; + }); + + it("non-monitor caller cannot invoke handleBorrowCapExceeding", async () => { + await expect(executor.connect(randomSigner).handleBorrowCapExceeding(VUSDC)).to.be.reverted; + }); + + it("handler reverts with MarketNotConfigured for a market never registered", async () => { + await expect(executor.connect(monitorSigner).handleLTVAdjust(UNLISTED_MARKET, 0)).to.be.revertedWithCustomError( + executor, + "MarketNotConfigured", + ); + }); + }); + + describe("Post-VIP behaviour — end-to-end signal → Executor → EBrake → Comptroller", () => { + it("handleLTVAdjust(newCF >= currentCF) traverses Executor + EBrake without reverting (idempotent path)", async () => { + // Forcing a real CF decrease at this fork block hits Comptroller's resilient-oracle + // validation ("invalid resilient oracle price"). Setting adjustedLTV to the current CF + // exercises the Monitor → Executor → EBrake auth chain — EBrake.decreaseCF then skips + // every pool's comptroller call (newCF >= currentCF → continue) and the Executor still + // emits the event. State-changing E2E is covered below via cap adjustments. + const [, currentCF] = await comptroller.markets(VUSDC); + expect(currentCF).to.be.gt(0); + + await expect(executor.connect(monitorSigner).handleLTVAdjust(VUSDC, currentCF)) + .to.emit(executor, "LTVAdjusted") + .withArgs(SIGNAL_MONITOR, VUSDC, currentCF); + }); + + it("handleCapAdjust(BORROW) lowers the borrow cap on the Comptroller and rejects below minBorrowCap", async () => { + const currentCap: BigNumber = await comptroller.borrowCaps(VUSDC); + const { minBorrowCap } = await executor.marketConfigs(VUSDC); + + // Below the configured floor — must revert. + await expect( + executor.connect(monitorSigner).handleCapAdjust(VUSDC, CAP_TYPE_BORROW, minBorrowCap.sub(1)), + ).to.be.revertedWithCustomError(executor, "CapBelowMinimum"); + + // Tighten within bounds. + const newCap = currentCap.sub(1); + await expect(executor.connect(monitorSigner).handleCapAdjust(VUSDC, CAP_TYPE_BORROW, newCap)) + .to.emit(executor, "CapAdjusted") + .withArgs(SIGNAL_MONITOR, VUSDC, CAP_TYPE_BORROW, currentCap, newCap); + + expect(await comptroller.borrowCaps(VUSDC)).to.equal(newCap); + }); + + it("handleCapAdjust(SUPPLY) lowers the supply cap on the Comptroller and rejects below minSupplyCap", async () => { + const currentCap: BigNumber = await comptroller.supplyCaps(VUSDC); + const { minSupplyCap } = await executor.marketConfigs(VUSDC); + + await expect( + executor.connect(monitorSigner).handleCapAdjust(VUSDC, CAP_TYPE_SUPPLY, minSupplyCap.sub(1)), + ).to.be.revertedWithCustomError(executor, "CapBelowMinimum"); + + const newCap = currentCap.sub(1); + await expect(executor.connect(monitorSigner).handleCapAdjust(VUSDC, CAP_TYPE_SUPPLY, newCap)) + .to.emit(executor, "CapAdjusted") + .withArgs(SIGNAL_MONITOR, VUSDC, CAP_TYPE_SUPPLY, currentCap, newCap); + + expect(await comptroller.supplyCaps(VUSDC)).to.equal(newCap); + }); + + it("handleSupplyCapExceeding reverts with CapNotBreached on a healthy market", async () => { + await expect(executor.connect(monitorSigner).handleSupplyCapExceeding(VUSDC)).to.be.revertedWithCustomError( + executor, + "CapNotBreached", + ); + }); + + it("handleBorrowCapExceeding reverts with CapNotBreached on a healthy market", async () => { + await expect(executor.connect(monitorSigner).handleBorrowCapExceeding(VUSDC)).to.be.revertedWithCustomError( + executor, + "CapNotBreached", + ); + }); + + it("handler reverts with MarketDisabled once config.enabled is flipped off", async () => { + const { minBorrowCap, minSupplyCap } = await executor.marketConfigs(VUSDC); + await executor + .connect(criticalTimelockSigner) + .setMarketConfig(VUSDC, { minBorrowCap, minSupplyCap, enabled: false }); + + await expect(executor.connect(monitorSigner).handleLTVAdjust(VUSDC, 0)).to.be.revertedWithCustomError( + executor, + "MarketDisabled", + ); + }); + }); + + describe("Post-VIP behaviour — treasury transfer", () => { + it("treasury USDT balance decreased by 25,000 USDT", async () => { + expect(await usdt.balanceOf(VTREASURY)).to.equal(treasuryUsdtBefore.sub(USDT_AMOUNT)); + }); + + it("Flux marketing wallet received 25,000 USDT", async () => { + expect(await usdt.balanceOf(FLUX_MARKETING_WALLET)).to.equal(fluxWalletUsdtBefore.add(USDT_AMOUNT)); + }); + }); +}); diff --git a/simulations/vip-623/bsctestnet.ts b/simulations/vip-623/bsctestnet.ts new file mode 100644 index 000000000..ed8e20b11 --- /dev/null +++ b/simulations/vip-623/bsctestnet.ts @@ -0,0 +1,98 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { expectEvents, initMainnetUser } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import vip623Testnet, { + ACM, + EBRAKE, + EBRAKE_EXECUTOR_PERMS, + EXECUTOR, + EXECUTOR_GOVERNANCE_PERMS, + EXECUTOR_MONITOR_PERMS, + SIGNAL_MONITOR, +} from "../../vips/vip-623/bsctestnet"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; + +const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN } = NETWORK_ADDRESSES.bsctestnet; +const EXECUTOR_GOVERNANCE_ACCOUNTS = [GUARDIAN, NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; + +// TODO: set to a block after Executor is deployed on BSC testnet +const BLOCK_NUMBER = 0; + +forking(BLOCK_NUMBER, async () => { + let accessControlManager: Contract; + + let impersonatedExecutor: SignerWithAddress; + let impersonatedEBrake: SignerWithAddress; + + before(async () => { + accessControlManager = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, ACM); + + impersonatedExecutor = await initMainnetUser(EXECUTOR, ethers.utils.parseEther("1")); + impersonatedEBrake = await initMainnetUser(EBRAKE, ethers.utils.parseEther("1")); + }); + + describe("Pre-VIP behavior", () => { + it("Signal monitor should not yet have Executor action permissions", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const sig of EXECUTOR_MONITOR_PERMS) { + expect(await acm.isAllowedToCall(SIGNAL_MONITOR, sig)).to.equal(false, `unexpected permission: ${sig}`); + } + }); + + it("Executor should not yet have EBrake permissions", async () => { + const acm = accessControlManager.connect(impersonatedEBrake); + for (const sig of EBRAKE_EXECUTOR_PERMS) { + expect(await acm.isAllowedToCall(EXECUTOR, sig)).to.equal(false, `unexpected permission: ${sig}`); + } + }); + + it("Guardian and Timelocks should not yet have setMarketConfig permission on Executor", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const account of EXECUTOR_GOVERNANCE_ACCOUNTS) { + for (const sig of EXECUTOR_GOVERNANCE_PERMS) { + expect(await acm.isAllowedToCall(account, sig)).to.equal( + false, + `unexpected permission ${sig} for ${account}`, + ); + } + } + }); + }); + + testVip("VIP-623 [BNB Testnet] Configure tighten-only Executor", await vip623Testnet(), { + callbackAfterExecution: async txResponse => { + // RoleGranted: 4 (monitor on Executor) + 5 (Executor on EBrake) + 4 (Guardian + 3 timelocks setMarketConfig) = 13 + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [13]); + }, + }); + + describe("Post-VIP behavior", () => { + it("Signal monitor should have all Executor action permissions", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const sig of EXECUTOR_MONITOR_PERMS) { + expect(await acm.isAllowedToCall(SIGNAL_MONITOR, sig)).to.equal(true, `missing permission: ${sig}`); + } + }); + + it("Executor should have all EBrake permissions", async () => { + const acm = accessControlManager.connect(impersonatedEBrake); + for (const sig of EBRAKE_EXECUTOR_PERMS) { + expect(await acm.isAllowedToCall(EXECUTOR, sig)).to.equal(true, `missing permission: ${sig}`); + } + }); + + it("Guardian and Timelocks should have setMarketConfig permission on Executor", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const account of EXECUTOR_GOVERNANCE_ACCOUNTS) { + for (const sig of EXECUTOR_GOVERNANCE_PERMS) { + expect(await acm.isAllowedToCall(account, sig)).to.equal(true, `missing permission ${sig} for ${account}`); + } + } + }); + }); +}); diff --git a/vips/vip-623/bscmainnet.ts b/vips/vip-623/bscmainnet.ts new file mode 100644 index 000000000..fb47dae5d --- /dev/null +++ b/vips/vip-623/bscmainnet.ts @@ -0,0 +1,171 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import coreMarketCaps from "./coreMarketCaps.json"; + +const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN, VTREASURY } = NETWORK_ADDRESSES.bscmainnet; + +// Access Control Manager +export const ACM = NETWORK_ADDRESSES.bscmainnet.ACCESS_CONTROL_MANAGER; + +// Marketing transfer +export const USDT = "0x55d398326f99059fF775485246999027B3197955"; +export const FLUX_MARKETING_WALLET = "0xBE0EdB1F457334B8d2DfEb3627567137E745A00B"; +export const USDT_AMOUNT = "25000000000000000000000"; // 25,000 USDT (18 decimals) + +// EBrake (configured in VIP-610) +export const EBRAKE = "0x35eBaBB99c7Fb7ba0C90bCc26e5d55Cdf89C23Ec"; + +// Executor — tighten-only validation layer between off-chain signal monitors and EBrake +export const EXECUTOR = "0xDd541A1b065F9587b01815a390a4d4559D7b630F"; + +// Off-chain signal monitor authorized to call Executor action handlers +export const SIGNAL_MONITOR = "0x61859C84E0C6aB7B5A9801A962C660477f31a2D3"; + +// Executor action functions the signal monitor invokes +export const EXECUTOR_MONITOR_PERMS = [ + "handleLTVAdjust(address,uint256)", + "handleCapAdjust(address,uint8,uint256)", + "handleSupplyCapExceeding(address)", + "handleBorrowCapExceeding(address)", +]; + +// Executor governance function — sets per-market bounds (minBorrowCap, minSupplyCap, enabled) +export const EXECUTOR_GOVERNANCE_PERMS = ["setMarketConfig(address,(uint256,uint256,bool))"]; + +// EBrake functions the Executor calls +export const EBRAKE_EXECUTOR_PERMS = [ + "pauseBorrow(address)", + "pauseSupply(address)", + "decreaseCF(address,uint256)", + "setMarketBorrowCaps(address[],uint256[])", + "setMarketSupplyCaps(address[],uint256[])", +]; + +// Per-market Executor configs — fully baked by scripts/fetchCoreMarketCaps.ts. +// The script already applies VIP-622 overrides and drops unlisted / zero-floor markets, +// so the VIP just emits one setMarketConfig per entry. Each entry's `capSource` in +// coreMarketCaps.json records whether the floor came from live state or VIP-622. +export const CORE_POOL_MARKET_CONFIGS: { + address: string; + symbol: string; + minBorrowCap: string; + minSupplyCap: string; +}[] = coreMarketCaps.markets.map(m => ({ + address: m.address, + symbol: m.symbol, + minBorrowCap: m.minBorrowCap, + minSupplyCap: m.minSupplyCap, +})); + +const giveCallPermission = (contract: string, sig: string, account: string) => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [contract, sig, account], +}); + +export const vip623 = () => { + const meta = { + version: "v2", + title: "VIP-623 [BNB Chain] EBrake Executor Phase -1 Activation & Flux Campaign Funding", + description: `#### Summary + +This VIP bundles two BNB Chain actions: + +1. **EBrake Executor Contract — Phase -1 Activation** — Bring the new tighten-only Executor contract into governance control, enabling off-chain signal monitors to route emergency tightening actions through pre-validated on-chain bounds. The Executor cannot loosen parameters; all recovery still requires a governance VIP. +2. **Binance Wallet × Flux Campaign Funding** — Transfer of 25,000 USDT from the Venus Treasury to the joint Binance Wallet × Flux campaign address. Fluid Protocol contributes the matching amount to the same address as partner funding. + +#### Description + +**EBrake Executor Contract — Phase -1 Activation** + +The Executor is the validation layer between off-chain signal monitors and the EBrake contract. It validates parameter bounds on-chain and routes tightening actions to EBrake. The contract is tighten-only: it cannot raise LTV, raise caps, or unpause. All recovery actions go through a governance VIP. + +Once activated, a signal that detects a breach (e.g. supply cap exceeded via oracle drift) invokes the Executor, which validates the current on-chain state and forwards the tightening call to EBrake within a single transaction — without VIP latency. + +- Source PR: [VenusProtocol/venus-periphery#61](https://github.com/VenusProtocol/venus-periphery/pull/61) (VPD-925) +- Audits: CertiK (2026-04-27), Hashdit (2026-05-08) — reports filed in the PR under audits/ +- Depends on: VIP-610 (EBrake configuration), VPD-984 (EBrake Phase-0) + +**Binance Wallet × Flux Campaign Funding** + +Venus is co-funding a joint campaign with Binance Wallet and Fluid Protocol (Flux) on BNB Chain. Funding flows to a shared multisig address; Fluid contributes the matching amount via their own treasury action, outside this VIP. + +- From: Venus Treasury +- To: 0xBE0EdB1F457334B8d2DfEb3627567137E745A00B +- Token: USDT +- Amount: 25,000 USDT +- Partner contribution: Fluid Protocol — 25,000 USDT to the same address (separate action) + +#### Proposed Changes + +**1. Accept ownership of Executor and EBrake** + +**2. Grant Signal Monitor permissions on Executor action handlers** + +- Authorize the off-chain monitor to call handleLTVAdjust, handleCapAdjust, handleSupplyCapExceeding, handleBorrowCapExceeding + +**3. Grant Executor permissions on EBrake** + +- Authorize the Executor to call pauseBorrow, pauseSupply, decreaseCF, setMarketBorrowCaps, setMarketSupplyCaps on EBrake + +**4. Grant Guardian and all three Timelocks (Normal, Fast Track, Critical) permission to call setMarketConfig on Executor** + +- Lets governance set per-market bounds (minBorrowCap, minSupplyCap, enabled). Granting to all three timelocks + Guardian mirrors VIP-610 and lets Critical (~1h) disable a compromised market's automation instead of waiting 48h on Normal. + +**5. Initialise Executor market configs for every Core Pool market** + +- Call setMarketConfig on the Executor for each listed Core Pool vToken with enabled = true and per-market floors set to **20% of the effective borrow/supply cap**. Floors are baked by [scripts/fetchCoreMarketCaps.ts](scripts/fetchCoreMarketCaps.ts) into [vips/vip-623/coreMarketCaps.json](vips/vip-623/coreMarketCaps.json); the script already applies VIP-622 (PR #706) post-cap targets where applicable and drops unlisted / zero-floor markets, so this VIP can publish ahead of VIP-622 landing on-chain. + +**6. Transfer 25,000 USDT from Venus Treasury to Flux marketing wallet** + +- Funds the incoming Flux marketing campaign. Recipient: 0xBE0EdB1F457334B8d2DfEb3627567137E745A00B (multisig shared with Fluid team). + +#### References + +- [GitHub PR: VenusProtocol/venus-periphery#61](https://github.com/VenusProtocol/venus-periphery/pull/61) +- [Community post: May 2026 Risk Parameter Update / Asset Off-boarding](https://community.venus.io/t/may-2026-risk-parameter-update-asset-off-boarding/5785) +- VPD-925 — Phase -1 Executor`, + forDescription: "I agree that Venus Protocol should proceed with this proposal", + againstDescription: "I do not think that Venus Protocol should proceed with this proposal", + abstainDescription: "I am indifferent to whether Venus Protocol proceeds or not", + }; + + return makeProposal( + [ + // 1. Accept ownership + { target: EXECUTOR, signature: "acceptOwnership()", params: [] }, + { target: EBRAKE, signature: "acceptOwnership()", params: [] }, + + // 2. Signal monitor → Executor action handlers + ...EXECUTOR_MONITOR_PERMS.map(sig => giveCallPermission(EXECUTOR, sig, SIGNAL_MONITOR)), + + // 3. Executor → EBrake + ...EBRAKE_EXECUTOR_PERMS.map(sig => giveCallPermission(EBRAKE, sig, EXECUTOR)), + + // 4. Guardian + all timelocks → Executor governance function + ...[GUARDIAN, NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK].flatMap(account => + EXECUTOR_GOVERNANCE_PERMS.map(sig => giveCallPermission(EXECUTOR, sig, account)), + ), + + // 5. Initialise Executor market configs for every Core Pool market (20% floors from script snapshot) + ...CORE_POOL_MARKET_CONFIGS.map(m => ({ + target: EXECUTOR, + signature: "setMarketConfig(address,(uint256,uint256,bool))", + params: [m.address, [m.minBorrowCap, m.minSupplyCap, true]], + })), + + // 6. Transfer 25,000 USDT to Flux marketing wallet for upcoming campaign + { + target: VTREASURY, + signature: "withdrawTreasuryBEP20(address,uint256,address)", + params: [USDT, USDT_AMOUNT, FLUX_MARKETING_WALLET], + }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip623; diff --git a/vips/vip-623/bsctestnet.ts b/vips/vip-623/bsctestnet.ts new file mode 100644 index 000000000..df00cb89e --- /dev/null +++ b/vips/vip-623/bsctestnet.ts @@ -0,0 +1,83 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN } = NETWORK_ADDRESSES.bsctestnet; + +// Access Control Manager (BSC testnet) +export const ACM = "0x45f8a08F534f34A97187626E05d4b6648Eeaa9AA"; + +// EBrake testnet (deployed in VIP-661 Testnet addendum) +export const EBRAKE = "0x957c09e3Ac3d9e689244DC74307c94111FBa8B42"; + +// Executor — tighten-only validation layer between off-chain signal monitors and EBrake +// TODO: replace with deployed Executor testnet address +export const EXECUTOR = "0x0000000000000000000000000000000000000000"; + +// Off-chain signal monitor authorized to call Executor action handlers +// TODO: replace with final monitor EOA/contract address on testnet +export const SIGNAL_MONITOR = "0x0000000000000000000000000000000000000000"; + +export const EXECUTOR_MONITOR_PERMS = [ + "handleLTVAdjust(address,uint256)", + "handleCapAdjust(address,uint8,uint256)", + "handleSupplyCapExceeding(address)", + "handleBorrowCapExceeding(address)", +]; + +export const EXECUTOR_GOVERNANCE_PERMS = ["setMarketConfig(address,(uint256,uint256,bool))"]; + +export const EBRAKE_EXECUTOR_PERMS = [ + "pauseBorrow(address)", + "pauseSupply(address)", + "decreaseCF(address,uint256)", + "setMarketBorrowCaps(address[],uint256[])", + "setMarketSupplyCaps(address[],uint256[])", +]; + +const giveCallPermission = (contract: string, sig: string, account: string) => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [contract, sig, account], +}); + +export const vip623Testnet = () => { + const meta = { + version: "v2", + title: "VIP-623 [BNB Testnet] Configure tighten-only Executor for signal-driven risk parameter control", + description: `#### Summary + +Configures the **Executor** contract on BSC testnet — tighten-only validation layer between off-chain signal monitors and EBrake. + +1. Grant signal monitor permissions on Executor action handlers (handleLTVAdjust, handleCapAdjust, handleSupplyCapExceeding, handleBorrowCapExceeding) +2. Grant Executor permissions on EBrake (pauseBorrow, pauseSupply, decreaseCF, setMarketBorrowCaps, setMarketSupplyCaps) +3. Grant Guardian and all three Timelocks (Normal, Fast Track, Critical) permission to call setMarketConfig on Executor + +#### References + +- [GitHub PR: VenusProtocol/venus-periphery#61](https://github.com/VenusProtocol/venus-periphery/pull/61) +- VPD-925 — Phase -1 Executor`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + // 1. Signal monitor → Executor action handlers + ...EXECUTOR_MONITOR_PERMS.map(sig => giveCallPermission(EXECUTOR, sig, SIGNAL_MONITOR)), + + // 2. Executor → EBrake + ...EBRAKE_EXECUTOR_PERMS.map(sig => giveCallPermission(EBRAKE, sig, EXECUTOR)), + + // 3. Guardian + all timelocks → Executor governance function + ...[GUARDIAN, NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK].flatMap(account => + EXECUTOR_GOVERNANCE_PERMS.map(sig => giveCallPermission(EXECUTOR, sig, account)), + ), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip623Testnet; diff --git a/vips/vip-623/coreMarketCaps.json b/vips/vip-623/coreMarketCaps.json new file mode 100644 index 000000000..438462474 --- /dev/null +++ b/vips/vip-623/coreMarketCaps.json @@ -0,0 +1,391 @@ +{ + "_meta": { + "network": "bscmainnet", + "comptroller": "0xfD36E2c2a6789Db23113685031d7F16329158384", + "corePoolId": 0, + "block": 98413702, + "fetchedAt": "2026-05-15T10:23:37.041Z", + "floorPercent": 20, + "vip622DataSource": "vips/vip-622/data/bscmainnet.ts (PR https://github.com/VenusProtocol/vips/pull/706)" + }, + "markets": [ + { + "address": "0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8", + "symbol": "vUSDC", + "capSource": "vip-622", + "effectiveBorrowCap": "324000000000000000000000000", + "effectiveSupplyCap": "360000000000000000000000000", + "minBorrowCap": "64800000000000000000000000", + "minSupplyCap": "72000000000000000000000000" + }, + { + "address": "0xfD5840Cd36d94D7229439859C0112a4185BC0255", + "symbol": "vUSDT", + "capSource": "live", + "effectiveBorrowCap": "450000000000000000000000000", + "effectiveSupplyCap": "600000000000000000000000000", + "minBorrowCap": "90000000000000000000000000", + "minSupplyCap": "120000000000000000000000000" + }, + { + "address": "0x151B1e2635A717bcDc836ECd6FbB62B674FE3E1D", + "symbol": "vXVS", + "capSource": "vip-622", + "effectiveBorrowCap": "0", + "effectiveSupplyCap": "1850000000000000000000000", + "minBorrowCap": "0", + "minSupplyCap": "370000000000000000000000" + }, + { + "address": "0xA07c5b74C9B40447a954e1466938b865b6BBea36", + "symbol": "vBNB", + "capSource": "live", + "effectiveBorrowCap": "2008000000000000000000000", + "effectiveSupplyCap": "2672000000000000000000000", + "minBorrowCap": "401600000000000000000000", + "minSupplyCap": "534400000000000000000000" + }, + { + "address": "0x882C173bC7Ff3b7786CA16dfeD3DFFfb9Ee7847B", + "symbol": "vBTC", + "capSource": "live", + "effectiveBorrowCap": "3531000000000000000000", + "effectiveSupplyCap": "22770000000000000000000", + "minBorrowCap": "706200000000000000000", + "minSupplyCap": "4554000000000000000000" + }, + { + "address": "0xf508fCD89b8bd15579dc79A6827cB4686A3592c8", + "symbol": "vETH", + "capSource": "vip-622", + "effectiveBorrowCap": "37000000000000000000000", + "effectiveSupplyCap": "72000000000000000000000", + "minBorrowCap": "7400000000000000000000", + "minSupplyCap": "14400000000000000000000" + }, + { + "address": "0x57A5297F2cB2c0AaC9D554660acd6D385Ab50c6B", + "symbol": "vLTC", + "capSource": "vip-622", + "effectiveBorrowCap": "8000000000000000000000", + "effectiveSupplyCap": "50000000000000000000000", + "minBorrowCap": "1600000000000000000000", + "minSupplyCap": "10000000000000000000000" + }, + { + "address": "0xB248a295732e0225acd3337607cc01068e3b9c10", + "symbol": "vXRP", + "capSource": "vip-622", + "effectiveBorrowCap": "1000000000000000000000000", + "effectiveSupplyCap": "7500000000000000000000000", + "minBorrowCap": "200000000000000000000000", + "minSupplyCap": "1500000000000000000000000" + }, + { + "address": "0x5F0388EBc2B94FA8E123F404b79cCF5f40b29176", + "symbol": "vBCH", + "capSource": "vip-622", + "effectiveBorrowCap": "1000000000000000000000", + "effectiveSupplyCap": "5000000000000000000000", + "minBorrowCap": "200000000000000000000", + "minSupplyCap": "1000000000000000000000" + }, + { + "address": "0x1610bc33319e9398de5f57B33a5b184c806aD217", + "symbol": "vDOT", + "capSource": "vip-622", + "effectiveBorrowCap": "400000000000000000000000", + "effectiveSupplyCap": "1200000000000000000000000", + "minBorrowCap": "80000000000000000000000", + "minSupplyCap": "240000000000000000000000" + }, + { + "address": "0x650b940a1033B8A1b1873f78730FcFC73ec11f1f", + "symbol": "vLINK", + "capSource": "vip-622", + "effectiveBorrowCap": "20000000000000000000000", + "effectiveSupplyCap": "400000000000000000000000", + "minBorrowCap": "4000000000000000000000", + "minSupplyCap": "80000000000000000000000" + }, + { + "address": "0x334b3eCB4DCa3593BCCC3c7EBD1A1C1d1780FBF1", + "symbol": "vDAI", + "capSource": "vip-622", + "effectiveBorrowCap": "7500000000000000000000000", + "effectiveSupplyCap": "13910000000000000000000000", + "minBorrowCap": "1500000000000000000000000", + "minSupplyCap": "2782000000000000000000000" + }, + { + "address": "0x9A0AF7FDb2065Ce470D72664DE73cAE409dA28Ec", + "symbol": "vADA", + "capSource": "vip-622", + "effectiveBorrowCap": "3000000000000000000000000", + "effectiveSupplyCap": "15000000000000000000000000", + "minBorrowCap": "600000000000000000000000", + "minSupplyCap": "3000000000000000000000000" + }, + { + "address": "0xec3422Ef92B2fb59e84c8B02Ba73F1fE84Ed8D71", + "symbol": "vDOGE", + "capSource": "vip-622", + "effectiveBorrowCap": "300000000000000", + "effectiveSupplyCap": "8000000000000000", + "minBorrowCap": "60000000000000", + "minSupplyCap": "1600000000000000" + }, + { + "address": "0x86aC3974e2BD0d60825230fa6F355fF11409df5c", + "symbol": "vCAKE", + "capSource": "vip-622", + "effectiveBorrowCap": "19200000000000000000000000", + "effectiveSupplyCap": "24000000000000000000000000", + "minBorrowCap": "3840000000000000000000000", + "minSupplyCap": "4800000000000000000000000" + }, + { + "address": "0x26DA28954763B92139ED49283625ceCAf52C6f94", + "symbol": "vAAVE", + "capSource": "vip-622", + "effectiveBorrowCap": "3000000000000000000000", + "effectiveSupplyCap": "20000000000000000000000", + "minBorrowCap": "600000000000000000000", + "minSupplyCap": "4000000000000000000000" + }, + { + "address": "0xC5D3466aA484B040eE977073fcF337f2c00071c1", + "symbol": "vTRX", + "capSource": "vip-622", + "effectiveBorrowCap": "1000000000000", + "effectiveSupplyCap": "3000000000000", + "minBorrowCap": "200000000000", + "minSupplyCap": "600000000000" + }, + { + "address": "0x6CFdEc747f37DAf3b87a35a1D9c8AD3063A1A8A0", + "symbol": "vWBETH", + "capSource": "vip-622", + "effectiveBorrowCap": "1000000000000000000000", + "effectiveSupplyCap": "40000000000000000000000", + "minBorrowCap": "200000000000000000000", + "minSupplyCap": "8000000000000000000000" + }, + { + "address": "0x27FF564707786720C71A2e5c1490A63266683612", + "symbol": "vUNI", + "capSource": "vip-622", + "effectiveBorrowCap": "200000000000000000000000", + "effectiveSupplyCap": "2200000000000000000000000", + "minBorrowCap": "40000000000000000000000", + "minSupplyCap": "440000000000000000000000" + }, + { + "address": "0xC4eF4229FEc74Ccfe17B2bdeF7715fAC740BA0ba", + "symbol": "vFDUSD", + "capSource": "vip-622", + "effectiveBorrowCap": "20000000000000000000000000", + "effectiveSupplyCap": "37000000000000000000000000", + "minBorrowCap": "4000000000000000000000000", + "minSupplyCap": "7400000000000000000000000" + }, + { + "address": "0x4d41a36D04D97785bcEA57b057C412b278e6Edcc", + "symbol": "vTWT", + "capSource": "vip-622", + "effectiveBorrowCap": "50000000000000000000000", + "effectiveSupplyCap": "2000000000000000000000000", + "minBorrowCap": "10000000000000000000000", + "minSupplyCap": "400000000000000000000000" + }, + { + "address": "0xf841cb62c19fCd4fF5CD0AaB5939f3140BaaC3Ea", + "symbol": "vSolvBTC", + "capSource": "vip-622", + "effectiveBorrowCap": "20000000000000000000", + "effectiveSupplyCap": "3000000000000000000000", + "minBorrowCap": "4000000000000000000", + "minSupplyCap": "600000000000000000000" + }, + { + "address": "0xBf515bA4D1b52FFdCeaBF20d31D705Ce789F2cEC", + "symbol": "vSOL", + "capSource": "live", + "effectiveBorrowCap": "18000000000000000000000", + "effectiveSupplyCap": "72000000000000000000000", + "minBorrowCap": "3600000000000000000000", + "minSupplyCap": "14400000000000000000000" + }, + { + "address": "0x689E0daB47Ab16bcae87Ec18491692BF621Dc6Ab", + "symbol": "vlisUSD", + "capSource": "vip-622", + "effectiveBorrowCap": "4000000000000000000000000", + "effectiveSupplyCap": "5000000000000000000000000", + "minBorrowCap": "800000000000000000000000", + "minSupplyCap": "1000000000000000000000000" + }, + { + "address": "0x9e4E5fed5Ac5B9F732d0D850A615206330Bf1866", + "symbol": "vPT-sUSDE-26JUN2025", + "capSource": "live", + "effectiveBorrowCap": "0", + "effectiveSupplyCap": "2000000000000000000000000", + "minBorrowCap": "0", + "minSupplyCap": "400000000000000000000000" + }, + { + "address": "0x699658323d58eE25c69F1a29d476946ab011bD18", + "symbol": "vsUSDe", + "capSource": "live", + "effectiveBorrowCap": "0", + "effectiveSupplyCap": "4000000000000000000000000", + "minBorrowCap": "0", + "minSupplyCap": "800000000000000000000000" + }, + { + "address": "0x74ca6930108F775CC667894EEa33843e691680d7", + "symbol": "vUSDe", + "capSource": "live", + "effectiveBorrowCap": "1600000000000000000000000", + "effectiveSupplyCap": "2000000000000000000000000", + "minBorrowCap": "320000000000000000000000", + "minSupplyCap": "400000000000000000000000" + }, + { + "address": "0x0C1DA220D301155b87318B90692Da8dc43B67340", + "symbol": "vUSD1", + "capSource": "vip-622", + "effectiveBorrowCap": "4000000000000000000000000", + "effectiveSupplyCap": "5000000000000000000000000", + "minBorrowCap": "800000000000000000000000", + "minSupplyCap": "1000000000000000000000000" + }, + { + "address": "0xd804dE60aFD05EE6B89aab5D152258fD461B07D5", + "symbol": "vxSolvBTC", + "capSource": "vip-622", + "effectiveBorrowCap": "0", + "effectiveSupplyCap": "1200000000000000000000", + "minBorrowCap": "0", + "minSupplyCap": "240000000000000000000" + }, + { + "address": "0xCC1dB43a06d97f736C7B045AedD03C6707c09BDF", + "symbol": "vasBNB", + "capSource": "vip-622", + "effectiveBorrowCap": "0", + "effectiveSupplyCap": "216000000000000000000000", + "minBorrowCap": "0", + "minSupplyCap": "43200000000000000000000" + }, + { + "address": "0x6bCa74586218dB34cdB402295796b79663d816e9", + "symbol": "vWBNB", + "capSource": "live", + "effectiveBorrowCap": "2008000000000000000000000", + "effectiveSupplyCap": "2672000000000000000000000", + "minBorrowCap": "401600000000000000000000", + "minSupplyCap": "534400000000000000000000" + }, + { + "address": "0x89c910Eb8c90df818b4649b508Ba22130Dc73Adc", + "symbol": "vslisBNB", + "capSource": "vip-622", + "effectiveBorrowCap": "0", + "effectiveSupplyCap": "2000000000000000000000", + "minBorrowCap": "0", + "minSupplyCap": "400000000000000000000" + }, + { + "address": "0x3d5E269787d562b74aCC55F18Bd26C5D09Fa245E", + "symbol": "vU", + "capSource": "live", + "effectiveBorrowCap": "210000000000000000000000000", + "effectiveSupplyCap": "210000000000000000000000000", + "minBorrowCap": "42000000000000000000000000", + "minSupplyCap": "42000000000000000000000000" + }, + { + "address": "0x6d3BD68E90B42615cb5abF4B8DE92b154ADc435e", + "symbol": "vPT-clisBNB-25JUN2026", + "capSource": "vip-622", + "effectiveBorrowCap": "0", + "effectiveSupplyCap": "25000000000000000000000", + "minBorrowCap": "0", + "minSupplyCap": "5000000000000000000000" + }, + { + "address": "0x92e6Ea74a1A3047DabF4186405a21c7D63a0612A", + "symbol": "vXAUM", + "capSource": "live", + "effectiveBorrowCap": "0", + "effectiveSupplyCap": "200000000000000000000", + "minBorrowCap": "0", + "minSupplyCap": "40000000000000000000" + } + ], + "skipped": [ + { + "address": "0x95c78222B3D6e262426483D42CfA53685A67Ab9D", + "symbol": "vBUSD", + "reason": "live 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0x2fF3d0F6990a40261c66E1ff2017aCBc282EB6d0", + "symbol": "vSXP", + "reason": "live 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0xf91d58b5aE142DAcC749f58A49FCBac340Cb0343", + "symbol": "vFIL", + "reason": "vip-622 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0x972207A639CC1B374B893cc33Fa251b55CEB7c07", + "symbol": "vBETH", + "reason": "live 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0xeBD0070237a0713E8D94fEf1B728d3d993d290ef", + "reason": "not listed in core pool (poolId 0)" + }, + { + "address": "0x5c9476FcD6a4F9a3654139721c949c2233bBbBc8", + "symbol": "vMATIC", + "reason": "live 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0x08CEB3F4a7ed3500cA0982bcd0FC7816688084c3", + "symbol": "vTUSDOLD", + "reason": "live 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0x61eDcFe8Dd6bA3c891CB9bEc2dc7657B3B422E93", + "symbol": "vTRXOLD", + "reason": "live 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0x78366446547D062f45b4C0f320cDaa6d710D87bb", + "reason": "not listed in core pool (poolId 0)" + }, + { + "address": "0xb91A659E88B51474767CD97EF3196A3e7cEDD2c8", + "reason": "not listed in core pool (poolId 0)" + }, + { + "address": "0xBf762cd5991cA1DCdDaC9ae5C638F5B5Dc3Bee6E", + "symbol": "vTUSD", + "reason": "vip-622 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0x86e06EAfa6A1eA631Eab51DE500E3D474933739f", + "symbol": "vTHE", + "reason": "vip-622 20% floors are both zero (delisted / dormant)" + }, + { + "address": "0x6D0cDb3355c93A0cD20071aBbb3622731a95c73E", + "reason": "not listed in core pool (poolId 0)" + } + ] +}