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..5bbe4eace --- /dev/null +++ b/simulations/vip-701/bscmainnet.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 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, 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; + +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-701 [BNB Chain] Configure tighten-only Executor", await vip701(), { + 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/simulations/vip-701/bsctestnet.ts b/simulations/vip-701/bsctestnet.ts new file mode 100644 index 000000000..02e51792f --- /dev/null +++ b/simulations/vip-701/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 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, 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-701 [BNB Testnet] Configure tighten-only Executor", await vip701Testnet(), { + 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-701/bscmainnet.ts b/vips/vip-701/bscmainnet.ts new file mode 100644 index 000000000..97885c647 --- /dev/null +++ b/vips/vip-701/bscmainnet.ts @@ -0,0 +1,98 @@ +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.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)", + "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[])", +]; + +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\`, \`handleSupplyCapExceeding\`, \`handleBorrowCapExceeding\` + +**2. Grant Executor permissions on EBrake** + +- Authorize the Executor to call \`pauseBorrow\`, \`pauseSupply\`, \`decreaseCF\`, \`setMarketBorrowCaps\`, \`setMarketSupplyCaps\` on EBrake + +**3. 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. + +#### 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. 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 vip701; diff --git a/vips/vip-701/bsctestnet.ts b/vips/vip-701/bsctestnet.ts new file mode 100644 index 000000000..34b4f9d7f --- /dev/null +++ b/vips/vip-701/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 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, 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 vip701Testnet;