From c8c6507eb0120f10844c60c0b0dddd756531ba3e Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 22 Apr 2026 17:07:08 +0530 Subject: [PATCH 1/2] feat: add VIP-701 to configure tighten-only Executor on BSC Wires the Phase -1 Executor contract between off-chain signal monitors and EBrake. Grants monitor perms on Executor action handlers, Executor perms on EBrake, and Normal Timelock perm on setMarketConfig. Mainnet and testnet scaffolds with shared perm arrays imported from the VIP for single source of truth. --- .../vip-701/abi/AccessControlManager.json | 360 ++++++++++++++++++ simulations/vip-701/bscmainnet.ts | 91 +++++ simulations/vip-701/bsctestnet.ts | 90 +++++ vips/vip-701/bscmainnet.ts | 97 +++++ vips/vip-701/bsctestnet.ts | 81 ++++ 5 files changed, 719 insertions(+) create mode 100644 simulations/vip-701/abi/AccessControlManager.json create mode 100644 simulations/vip-701/bscmainnet.ts create mode 100644 simulations/vip-701/bsctestnet.ts create mode 100644 vips/vip-701/bscmainnet.ts create mode 100644 vips/vip-701/bsctestnet.ts diff --git a/simulations/vip-701/abi/AccessControlManager.json b/simulations/vip-701/abi/AccessControlManager.json new file mode 100644 index 000000000..4a118fcc4 --- /dev/null +++ b/simulations/vip-701/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-701/bscmainnet.ts b/simulations/vip-701/bscmainnet.ts new file mode 100644 index 000000000..d2419b466 --- /dev/null +++ b/simulations/vip-701/bscmainnet.ts @@ -0,0 +1,91 @@ +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 vip701, { + ACM, + EBRAKE, + EBRAKE_EXECUTOR_PERMS, + EXECUTOR, + EXECUTOR_GOVERNANCE_PERMS, + EXECUTOR_MONITOR_PERMS, + SIGNAL_MONITOR, +} from "../../vips/vip-701/bscmainnet"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; + +const { NORMAL_TIMELOCK } = NETWORK_ADDRESSES.bscmainnet; + +// TODO: set to a block after Executor is deployed on BSC mainnet +const BLOCK_NUMBER = 0; + +forking(BLOCK_NUMBER, async () => { + let accessControlManager: Contract; + + // ACM checks msg.sender against the stored contract address — impersonate the host. + 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("Normal Timelock should not yet have setMarketConfig permission on Executor", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const sig of EXECUTOR_GOVERNANCE_PERMS) { + expect(await acm.isAllowedToCall(NORMAL_TIMELOCK, sig)).to.equal(false, `unexpected permission: ${sig}`); + } + }); + }); + + testVip("VIP-701 [BNB Chain] Configure tighten-only Executor", await vip701(), { + callbackAfterExecution: async txResponse => { + // RoleGranted: 4 (monitor on Executor) + 5 (Executor on EBrake) + 1 (timelock setMarketConfig) = 10 + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [10]); + }, + }); + + 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("Normal Timelock should have setMarketConfig permission on Executor", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const sig of EXECUTOR_GOVERNANCE_PERMS) { + expect(await acm.isAllowedToCall(NORMAL_TIMELOCK, sig)).to.equal(true, `missing permission: ${sig}`); + } + }); + }); +}); diff --git a/simulations/vip-701/bsctestnet.ts b/simulations/vip-701/bsctestnet.ts new file mode 100644 index 000000000..2f3e5bc8a --- /dev/null +++ b/simulations/vip-701/bsctestnet.ts @@ -0,0 +1,90 @@ +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 vip701Testnet, { + ACM, + EBRAKE, + EBRAKE_EXECUTOR_PERMS, + EXECUTOR, + EXECUTOR_GOVERNANCE_PERMS, + EXECUTOR_MONITOR_PERMS, + SIGNAL_MONITOR, +} from "../../vips/vip-701/bsctestnet"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; + +const { NORMAL_TIMELOCK } = NETWORK_ADDRESSES.bsctestnet; + +// 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("Normal Timelock should not yet have setMarketConfig permission on Executor", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const sig of EXECUTOR_GOVERNANCE_PERMS) { + expect(await acm.isAllowedToCall(NORMAL_TIMELOCK, sig)).to.equal(false, `unexpected permission: ${sig}`); + } + }); + }); + + testVip("VIP-701 [BNB Testnet] Configure tighten-only Executor", await vip701Testnet(), { + callbackAfterExecution: async txResponse => { + // RoleGranted: 4 (monitor on Executor) + 5 (Executor on EBrake) + 1 (timelock setMarketConfig) = 10 + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [10]); + }, + }); + + 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("Normal Timelock should have setMarketConfig permission on Executor", async () => { + const acm = accessControlManager.connect(impersonatedExecutor); + for (const sig of EXECUTOR_GOVERNANCE_PERMS) { + expect(await acm.isAllowedToCall(NORMAL_TIMELOCK, sig)).to.equal(true, `missing permission: ${sig}`); + } + }); + }); +}); diff --git a/vips/vip-701/bscmainnet.ts b/vips/vip-701/bscmainnet.ts new file mode 100644 index 000000000..cb904efbe --- /dev/null +++ b/vips/vip-701/bscmainnet.ts @@ -0,0 +1,97 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { NORMAL_TIMELOCK } = NETWORK_ADDRESSES.bscmainnet; + +// Access Control Manager +export const ACM = NETWORK_ADDRESSES.bscmainnet.ACCESS_CONTROL_MANAGER; + +// EBrake (configured in VIP-610) +export const EBRAKE = "0x35eBaBB99c7Fb7ba0C90bCc26e5d55Cdf89C23Ec"; + +// Executor — tighten-only validation layer between off-chain signal monitors and EBrake +// TODO: replace with deployed Executor address once deployment VIP merges +export const EXECUTOR = "0x0000000000000000000000000000000000000000"; + +// Off-chain signal monitor authorized to call Executor action handlers +// TODO: replace with final monitor EOA/contract address +export const SIGNAL_MONITOR = "0x0000000000000000000000000000000000000000"; + +// Executor action functions the signal monitor invokes +export const EXECUTOR_MONITOR_PERMS = [ + "handleLTVAdjust(address,uint256)", + "handleCapAdjust(address,uint8,uint256)", + "handleSupplyHalt(address)", + "handleBorrowHalt(address)", +]; + +// Executor governance function — sets per-market bounds (originalLTV, minBorrowCap, +// minSupplyCap, originalBorrowCap, originalSupplyCap, enabled) +export const EXECUTOR_GOVERNANCE_PERMS = ["setMarketConfig(address,(uint256,uint256,uint256,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[])", +]; + +const giveCallPermission = (contract: string, sig: string, account: string) => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [contract, sig, account], +}); + +export const vip701 = () => { + const meta = { + version: "v2", + title: "VIP-701 [BNB Chain] Configure tighten-only Executor for signal-driven risk parameter control", + description: `#### Description + +This VIP configures the **Executor** contract on BNB Chain mainnet — the validation layer between off-chain signal monitors and EBrake. It validates bounds on-chain and routes tightening actions to EBrake; it cannot loosen parameters. Recovery is exclusively through governance VIPs. + +Depends on: VIP-610 (EBrake configuration), VPD-984 (EBrake Phase-0). + +#### Proposed Changes + +**1. Grant Signal Monitor permissions on Executor action handlers** + +- Authorize the off-chain monitor to call \`handleLTVAdjust\`, \`handleCapAdjust\`, \`handleSupplyHalt\`, \`handleBorrowHalt\` + +**2. Grant Executor permissions on EBrake** + +- Authorize the Executor to call \`pauseBorrow\`, \`pauseSupply\`, \`decreaseCF\`, \`setMarketBorrowCaps\`, \`setMarketSupplyCaps\` on EBrake + +**3. Grant Normal Timelock permission to call \`setMarketConfig\` on Executor** + +- Lets governance set per-market bounds (\`originalLTV\`, \`minBorrowCap\`, \`minSupplyCap\`, \`originalBorrowCap\`, \`originalSupplyCap\`, \`enabled\`) + +#### References + +- [GitHub PR: VenusProtocol/venus-periphery#61](https://github.com/VenusProtocol/venus-periphery/pull/61) +- 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. 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. Normal Timelock → Executor governance function + ...EXECUTOR_GOVERNANCE_PERMS.map(sig => giveCallPermission(EXECUTOR, sig, NORMAL_TIMELOCK)), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip701; diff --git a/vips/vip-701/bsctestnet.ts b/vips/vip-701/bsctestnet.ts new file mode 100644 index 000000000..82c175485 --- /dev/null +++ b/vips/vip-701/bsctestnet.ts @@ -0,0 +1,81 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { NORMAL_TIMELOCK } = 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)", + "handleSupplyHalt(address)", + "handleBorrowHalt(address)", +]; + +export const EXECUTOR_GOVERNANCE_PERMS = ["setMarketConfig(address,(uint256,uint256,uint256,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 vip701Testnet = () => { + const meta = { + version: "v2", + title: "VIP-701 [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, handleSupplyHalt, handleBorrowHalt) +2. Grant Executor permissions on EBrake (pauseBorrow, pauseSupply, decreaseCF, setMarketBorrowCaps, setMarketSupplyCaps) +3. Grant Normal Timelock 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. Normal Timelock → Executor governance function + ...EXECUTOR_GOVERNANCE_PERMS.map(sig => giveCallPermission(EXECUTOR, sig, NORMAL_TIMELOCK)), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip701Testnet; From 355eea4d7b17a2b92c6753261a20f17e69c34a9a Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 22 Apr 2026 21:42:17 +0530 Subject: [PATCH 2/2] fix(vip-701): correct ACM strings and broaden setMarketConfig grants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `handleSupplyHalt` → `handleSupplyCapExceeding` (orphaned role: no such function on Executor) - `handleBorrowHalt` → `handleBorrowCapExceeding` (same) - `setMarketConfig` tuple: 6-field phantom → 3-field `(minBorrowCap, minSupplyCap, enabled)` matching IExecutor.MarketConfig - Broaden `setMarketConfig` grant from Normal-only to Guardian + all three timelocks --- simulations/vip-701/bscmainnet.ts | 27 +++++++++++++++++---------- simulations/vip-701/bsctestnet.ts | 26 +++++++++++++++++--------- vips/vip-701/bscmainnet.ts | 23 ++++++++++++----------- vips/vip-701/bsctestnet.ts | 18 ++++++++++-------- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/simulations/vip-701/bscmainnet.ts b/simulations/vip-701/bscmainnet.ts index d2419b466..5bbe4eace 100644 --- a/simulations/vip-701/bscmainnet.ts +++ b/simulations/vip-701/bscmainnet.ts @@ -17,7 +17,8 @@ import vip701, { } from "../../vips/vip-701/bscmainnet"; import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; -const { NORMAL_TIMELOCK } = NETWORK_ADDRESSES.bscmainnet; +const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN } = NETWORK_ADDRESSES.bscmainnet; +const EXECUTOR_GOVERNANCE_ACCOUNTS = [GUARDIAN, NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; // TODO: set to a block after Executor is deployed on BSC mainnet const BLOCK_NUMBER = 0; @@ -25,7 +26,6 @@ const BLOCK_NUMBER = 0; forking(BLOCK_NUMBER, async () => { let accessControlManager: Contract; - // ACM checks msg.sender against the stored contract address — impersonate the host. let impersonatedExecutor: SignerWithAddress; let impersonatedEBrake: SignerWithAddress; @@ -51,18 +51,23 @@ forking(BLOCK_NUMBER, async () => { } }); - it("Normal Timelock should not yet have setMarketConfig permission on Executor", async () => { + it("Guardian and Timelocks should not yet have setMarketConfig permission on Executor", async () => { const acm = accessControlManager.connect(impersonatedExecutor); - for (const sig of EXECUTOR_GOVERNANCE_PERMS) { - expect(await acm.isAllowedToCall(NORMAL_TIMELOCK, sig)).to.equal(false, `unexpected permission: ${sig}`); + 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-701 [BNB Chain] Configure tighten-only Executor", await vip701(), { callbackAfterExecution: async txResponse => { - // RoleGranted: 4 (monitor on Executor) + 5 (Executor on EBrake) + 1 (timelock setMarketConfig) = 10 - await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [10]); + // RoleGranted: 4 (monitor on Executor) + 5 (Executor on EBrake) + 4 (Guardian + 3 timelocks setMarketConfig) = 13 + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [13]); }, }); @@ -81,10 +86,12 @@ forking(BLOCK_NUMBER, async () => { } }); - it("Normal Timelock should have setMarketConfig permission on Executor", async () => { + it("Guardian and Timelocks should have setMarketConfig permission on Executor", async () => { const acm = accessControlManager.connect(impersonatedExecutor); - for (const sig of EXECUTOR_GOVERNANCE_PERMS) { - expect(await acm.isAllowedToCall(NORMAL_TIMELOCK, sig)).to.equal(true, `missing permission: ${sig}`); + 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/simulations/vip-701/bsctestnet.ts b/simulations/vip-701/bsctestnet.ts index 2f3e5bc8a..02e51792f 100644 --- a/simulations/vip-701/bsctestnet.ts +++ b/simulations/vip-701/bsctestnet.ts @@ -17,7 +17,8 @@ import vip701Testnet, { } from "../../vips/vip-701/bsctestnet"; import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; -const { NORMAL_TIMELOCK } = NETWORK_ADDRESSES.bsctestnet; +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; @@ -50,18 +51,23 @@ forking(BLOCK_NUMBER, async () => { } }); - it("Normal Timelock should not yet have setMarketConfig permission on Executor", async () => { + it("Guardian and Timelocks should not yet have setMarketConfig permission on Executor", async () => { const acm = accessControlManager.connect(impersonatedExecutor); - for (const sig of EXECUTOR_GOVERNANCE_PERMS) { - expect(await acm.isAllowedToCall(NORMAL_TIMELOCK, sig)).to.equal(false, `unexpected permission: ${sig}`); + 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-701 [BNB Testnet] Configure tighten-only Executor", await vip701Testnet(), { callbackAfterExecution: async txResponse => { - // RoleGranted: 4 (monitor on Executor) + 5 (Executor on EBrake) + 1 (timelock setMarketConfig) = 10 - await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [10]); + // RoleGranted: 4 (monitor on Executor) + 5 (Executor on EBrake) + 4 (Guardian + 3 timelocks setMarketConfig) = 13 + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [13]); }, }); @@ -80,10 +86,12 @@ forking(BLOCK_NUMBER, async () => { } }); - it("Normal Timelock should have setMarketConfig permission on Executor", async () => { + it("Guardian and Timelocks should have setMarketConfig permission on Executor", async () => { const acm = accessControlManager.connect(impersonatedExecutor); - for (const sig of EXECUTOR_GOVERNANCE_PERMS) { - expect(await acm.isAllowedToCall(NORMAL_TIMELOCK, sig)).to.equal(true, `missing permission: ${sig}`); + 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-701/bscmainnet.ts b/vips/vip-701/bscmainnet.ts index cb904efbe..97885c647 100644 --- a/vips/vip-701/bscmainnet.ts +++ b/vips/vip-701/bscmainnet.ts @@ -2,7 +2,7 @@ import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { ProposalType } from "src/types"; import { makeProposal } from "src/utils"; -const { NORMAL_TIMELOCK } = NETWORK_ADDRESSES.bscmainnet; +const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN } = NETWORK_ADDRESSES.bscmainnet; // Access Control Manager export const ACM = NETWORK_ADDRESSES.bscmainnet.ACCESS_CONTROL_MANAGER; @@ -22,13 +22,12 @@ export const SIGNAL_MONITOR = "0x0000000000000000000000000000000000000000"; export const EXECUTOR_MONITOR_PERMS = [ "handleLTVAdjust(address,uint256)", "handleCapAdjust(address,uint8,uint256)", - "handleSupplyHalt(address)", - "handleBorrowHalt(address)", + "handleSupplyCapExceeding(address)", + "handleBorrowCapExceeding(address)", ]; -// Executor governance function — sets per-market bounds (originalLTV, minBorrowCap, -// minSupplyCap, originalBorrowCap, originalSupplyCap, enabled) -export const EXECUTOR_GOVERNANCE_PERMS = ["setMarketConfig(address,(uint256,uint256,uint256,uint256,uint256,bool))"]; +// 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 = [ @@ -59,15 +58,15 @@ Depends on: VIP-610 (EBrake configuration), VPD-984 (EBrake Phase-0). **1. Grant Signal Monitor permissions on Executor action handlers** -- Authorize the off-chain monitor to call \`handleLTVAdjust\`, \`handleCapAdjust\`, \`handleSupplyHalt\`, \`handleBorrowHalt\` +- Authorize the off-chain monitor to call \`handleLTVAdjust\`, \`handleCapAdjust\`, \`handleSupplyCapExceeding\`, \`handleBorrowCapExceeding\` **2. Grant Executor permissions on EBrake** - Authorize the Executor to call \`pauseBorrow\`, \`pauseSupply\`, \`decreaseCF\`, \`setMarketBorrowCaps\`, \`setMarketSupplyCaps\` on EBrake -**3. Grant Normal Timelock permission to call \`setMarketConfig\` on Executor** +**3. Grant Guardian and all three Timelocks (Normal, Fast Track, Critical) permission to call \`setMarketConfig\` on Executor** -- Lets governance set per-market bounds (\`originalLTV\`, \`minBorrowCap\`, \`minSupplyCap\`, \`originalBorrowCap\`, \`originalSupplyCap\`, \`enabled\`) +- 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. #### References @@ -86,8 +85,10 @@ Depends on: VIP-610 (EBrake configuration), VPD-984 (EBrake Phase-0). // 2. Executor → EBrake ...EBRAKE_EXECUTOR_PERMS.map(sig => giveCallPermission(EBRAKE, sig, EXECUTOR)), - // 3. Normal Timelock → Executor governance function - ...EXECUTOR_GOVERNANCE_PERMS.map(sig => giveCallPermission(EXECUTOR, sig, NORMAL_TIMELOCK)), + // 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, diff --git a/vips/vip-701/bsctestnet.ts b/vips/vip-701/bsctestnet.ts index 82c175485..34b4f9d7f 100644 --- a/vips/vip-701/bsctestnet.ts +++ b/vips/vip-701/bsctestnet.ts @@ -2,7 +2,7 @@ import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { ProposalType } from "src/types"; import { makeProposal } from "src/utils"; -const { NORMAL_TIMELOCK } = NETWORK_ADDRESSES.bsctestnet; +const { NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, GUARDIAN } = NETWORK_ADDRESSES.bsctestnet; // Access Control Manager (BSC testnet) export const ACM = "0x45f8a08F534f34A97187626E05d4b6648Eeaa9AA"; @@ -21,11 +21,11 @@ export const SIGNAL_MONITOR = "0x0000000000000000000000000000000000000000"; export const EXECUTOR_MONITOR_PERMS = [ "handleLTVAdjust(address,uint256)", "handleCapAdjust(address,uint8,uint256)", - "handleSupplyHalt(address)", - "handleBorrowHalt(address)", + "handleSupplyCapExceeding(address)", + "handleBorrowCapExceeding(address)", ]; -export const EXECUTOR_GOVERNANCE_PERMS = ["setMarketConfig(address,(uint256,uint256,uint256,uint256,uint256,bool))"]; +export const EXECUTOR_GOVERNANCE_PERMS = ["setMarketConfig(address,(uint256,uint256,bool))"]; export const EBRAKE_EXECUTOR_PERMS = [ "pauseBorrow(address)", @@ -49,9 +49,9 @@ export const vip701Testnet = () => { 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, handleSupplyHalt, handleBorrowHalt) +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 Normal Timelock permission to call setMarketConfig on Executor +3. Grant Guardian and all three Timelocks (Normal, Fast Track, Critical) permission to call setMarketConfig on Executor #### References @@ -70,8 +70,10 @@ Configures the **Executor** contract on BSC testnet — tighten-only validation // 2. Executor → EBrake ...EBRAKE_EXECUTOR_PERMS.map(sig => giveCallPermission(EBRAKE, sig, EXECUTOR)), - // 3. Normal Timelock → Executor governance function - ...EXECUTOR_GOVERNANCE_PERMS.map(sig => giveCallPermission(EXECUTOR, sig, NORMAL_TIMELOCK)), + // 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,