diff --git a/simulations/vip-664/bscmainnet.ts b/simulations/vip-664/bscmainnet.ts new file mode 100644 index 000000000..04cb283e3 --- /dev/null +++ b/simulations/vip-664/bscmainnet.ts @@ -0,0 +1,182 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { parseEther } from "ethers/lib/utils"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { expectEvents } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import vip664, { + ACM_AGGREGATOR, + DEFAULT_ADMIN_ROLE, + EXPECTED_PERMISSION_GRANTED_EVENTS, + INSTITUTIONAL_VAULT_CONTROLLER, + INSTITUTION_POSITION_TOKEN, + LIQUIDATION_ADAPTER, + LIQUIDATOR_WHITELIST, + PERMISSION_ENTRIES, + SETTLER_WHITELIST, +} from "../../vips/vip-664/bscmainnet"; +import ACM_AGGREGATOR_ABI from "./abi/ACMAggregator.json"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; +import INSTITUTION_POSITION_TOKEN_ABI from "./abi/InstitutionPositionToken.json"; +import INSTITUTIONAL_VAULT_CONTROLLER_ABI from "./abi/InstitutionalVaultController.json"; +import LIQUIDATION_ADAPTER_ABI from "./abi/LiquidationAdapter.json"; + +const { + NORMAL_TIMELOCK: NORMAL, + FAST_TRACK_TIMELOCK: FAST_TRACK, + CRITICAL_TIMELOCK: CRITICAL, + GUARDIAN, + ACCESS_CONTROL_MANAGER, +} = NETWORK_ADDRESSES.bscmainnet; + +const FORK_BLOCK = 96701105; + +// To make test names readable. +const LABEL: Record = { + [NORMAL]: "Normal", + [FAST_TRACK]: "FastTrack", + [CRITICAL]: "Critical", + [GUARDIAN]: "Guardian", +}; + +forking(FORK_BLOCK, async () => { + let accessControlManager: Contract; + let controller: Contract; + let liquidationAdapter: Contract; + let positionToken: Contract; + + before(async () => { + accessControlManager = new ethers.Contract(ACCESS_CONTROL_MANAGER, ACCESS_CONTROL_MANAGER_ABI, ethers.provider); + controller = new ethers.Contract( + INSTITUTIONAL_VAULT_CONTROLLER, + INSTITUTIONAL_VAULT_CONTROLLER_ABI, + ethers.provider, + ); + liquidationAdapter = new ethers.Contract(LIQUIDATION_ADAPTER, LIQUIDATION_ADAPTER_ABI, ethers.provider); + positionToken = new ethers.Contract(INSTITUTION_POSITION_TOKEN, INSTITUTION_POSITION_TOKEN_ABI, ethers.provider); + }); + + // Contracts deployed and deploy-script state is in place. + describe("Pre-VIP: verify deployments", () => { + it("InstitutionalVaultController proxy should be deployed", async () => { + expect(await ethers.provider.getCode(INSTITUTIONAL_VAULT_CONTROLLER)).to.not.equal("0x"); + }); + + it("LiquidationAdapter proxy should be deployed", async () => { + expect(await ethers.provider.getCode(LIQUIDATION_ADAPTER)).to.not.equal("0x"); + }); + + it("InstitutionPositionToken should be deployed", async () => { + expect(await ethers.provider.getCode(INSTITUTION_POSITION_TOKEN)).to.not.equal("0x"); + }); + + it("InstitutionPositionToken pendingOwner should be the controller proxy", async () => { + expect(await positionToken.pendingOwner()).to.equal(INSTITUTIONAL_VAULT_CONTROLLER); + }); + + it("controller pendingOwner should be Normal timelock", async () => { + expect(await controller.pendingOwner()).to.equal(NORMAL); + }); + + it("liquidationAdapter pendingOwner should be Normal timelock", async () => { + expect(await liquidationAdapter.pendingOwner()).to.equal(NORMAL); + }); + + it("liquidationAdapter protocolLiquidationShare should be 0.5e18", async () => { + expect(await liquidationAdapter.protocolLiquidationShare()).to.equal(parseEther("0.5")); + }); + + it("liquidationAdapter closeFactor should be 0.5e18", async () => { + expect(await liquidationAdapter.closeFactor()).to.equal(parseEther("0.5")); + }); + + it("controller liquidationAdapter should be address(0) before VIP", async () => { + expect(await controller.liquidationAdapter()).to.equal(ethers.constants.AddressZero); + }); + }); + + // None of the planned grants exist yet. + describe("Pre-VIP: ACM permissions not yet granted", () => { + for (const { target, fn, callers } of PERMISSION_ENTRIES) { + for (const account of callers) { + it(`${LABEL[account]} should NOT yet have permission: ${fn} on ${target}`, async () => { + expect(await accessControlManager.hasPermission(account, target, fn)).to.be.false; + }); + } + } + }); + + testVip("VIP-664 [BNB Chain] Configure Institutional Fixed Rate Vault System", await vip664(), { + callbackAfterExecution: async txResponse => { + await expectEvents( + txResponse, + [ACCESS_CONTROL_MANAGER_ABI], + ["PermissionGranted", "RoleGranted", "RoleRevoked"], + [EXPECTED_PERMISSION_GRANTED_EVENTS, EXPECTED_PERMISSION_GRANTED_EVENTS + 1, 1], + ); + await expectEvents( + txResponse, + [ACM_AGGREGATOR_ABI], + ["GrantPermissionsAdded", "GrantPermissionsExecuted"], + [1, 1], + ); + }, + }); + + // Every planned grant is now active. + describe("Post-VIP: ACM permissions granted", () => { + for (const { target, fn, callers } of PERMISSION_ENTRIES) { + for (const account of callers) { + it(`${LABEL[account]} should have permission: ${fn} on ${target}`, async () => { + expect(await accessControlManager.hasPermission(account, target, fn)).to.be.true; + }); + } + } + }); + + // Ownership accepted, adapter wired, admin role revoked. + describe("Post-VIP: ownership and wiring", () => { + it("controller owner should be Normal timelock", async () => { + expect(await controller.owner()).to.equal(NORMAL); + }); + + it("liquidationAdapter owner should be Normal timelock", async () => { + expect(await liquidationAdapter.owner()).to.equal(NORMAL); + }); + + it("InstitutionPositionToken owner should be the controller proxy", async () => { + expect(await positionToken.owner()).to.equal(INSTITUTIONAL_VAULT_CONTROLLER); + }); + + it("controller liquidationAdapter should be set to the adapter proxy", async () => { + expect(await controller.liquidationAdapter()).to.equal(LIQUIDATION_ADAPTER); + }); + + it("DEFAULT_ADMIN_ROLE should be revoked from ACM Aggregator", async () => { + expect(await accessControlManager.hasRole(DEFAULT_ADMIN_ROLE, ACM_AGGREGATOR)).to.be.false; + }); + }); + + // Dedicated operator addresses whitelisted; Guardian explicitly is not. + describe("Post-VIP: liquidator/settler whitelists", () => { + for (const account of LIQUIDATOR_WHITELIST) { + it(`${account} should be a whitelisted liquidator`, async () => { + expect(await liquidationAdapter.liquidatorWhitelist(account)).to.be.true; + }); + } + it("GUARDIAN should NOT be a whitelisted liquidator", async () => { + expect(await liquidationAdapter.liquidatorWhitelist(GUARDIAN)).to.be.false; + }); + + for (const account of SETTLER_WHITELIST) { + it(`${account} should be a whitelisted settler`, async () => { + expect(await liquidationAdapter.settlerWhitelist(account)).to.be.true; + }); + } + it("GUARDIAN should NOT be a whitelisted settler", async () => { + expect(await liquidationAdapter.settlerWhitelist(GUARDIAN)).to.be.false; + }); + }); +}); diff --git a/vips/vip-664/addGrantPermissions.ts b/vips/vip-664/addGrantPermissions.ts index d18cae838..0d24b68c1 100644 --- a/vips/vip-664/addGrantPermissions.ts +++ b/vips/vip-664/addGrantPermissions.ts @@ -1,6 +1,12 @@ -import { ethers } from "hardhat"; +import hre, { ethers } from "hardhat"; -import { ACM_AGGREGATOR, PERMISSIONS } from "./bsctestnet"; +import * as bscmainnet from "./bscmainnet"; +import * as bsctestnet from "./bsctestnet"; + +const NETWORK_CONFIG: Record = { + bsctestnet: { ACM_AGGREGATOR: bsctestnet.ACM_AGGREGATOR, PERMISSIONS: bsctestnet.PERMISSIONS }, + bscmainnet: { ACM_AGGREGATOR: bscmainnet.ACM_AGGREGATOR, PERMISSIONS: bscmainnet.PERMISSIONS }, +}; const ACM_AGGREGATOR_ABI = [ { @@ -30,6 +36,16 @@ const ACM_AGGREGATOR_ABI = [ ]; async function main() { + const networkName = hre.network.name; + const config = NETWORK_CONFIG[networkName]; + if (!config) { + throw new Error( + `addGrantPermissions: unsupported network "${networkName}". Use --network bsctestnet or --network bscmainnet.`, + ); + } + const { ACM_AGGREGATOR, PERMISSIONS } = config; + + console.log(`Network: ${networkName}, ACM Aggregator: ${ACM_AGGREGATOR}`); console.log(`Total PERMISSIONS to add: ${PERMISSIONS.length}`); const [signer] = await ethers.getSigners(); diff --git a/vips/vip-664/bscmainnet.ts b/vips/vip-664/bscmainnet.ts new file mode 100644 index 000000000..4fcb426d4 --- /dev/null +++ b/vips/vip-664/bscmainnet.ts @@ -0,0 +1,214 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { + NORMAL_TIMELOCK: NORMAL, + FAST_TRACK_TIMELOCK: FAST_TRACK, + CRITICAL_TIMELOCK: CRITICAL, + GUARDIAN, + ACCESS_CONTROL_MANAGER, +} = NETWORK_ADDRESSES.bscmainnet; + +export interface PermissionEntry { + target: string; + fn: string; + callers: string[]; +} + +// Deployed addresses (TODO: replace with mainnet addresses before proposing) +export const INSTITUTIONAL_VAULT_CONTROLLER = "0x0000000000000000000000000000000000000000"; +export const LIQUIDATION_ADAPTER = "0x0000000000000000000000000000000000000000"; +export const INSTITUTION_POSITION_TOKEN = "0x0000000000000000000000000000000000000000"; + +// ACM aggregator (mainnet) +export const ACM_AGGREGATOR = "0x8b443Ea6726E56DF4C4F62f80F0556bB9B2a7c64"; +export const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; +export const ACM_AGGREGATOR_INDEX = 1; + +export const LIQUIDATOR_WHITELIST: string[] = []; // TBD: populate before proposing. +export const SETTLER_WHITELIST: string[] = []; // TBD: populate before proposing. + +export const PERMISSION_ENTRIES: PermissionEntry[] = [ + // InstitutionalVaultController + { target: INSTITUTIONAL_VAULT_CONTROLLER, fn: "acceptPositionTokenOwnership()", callers: [NORMAL] }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "createVault(VaultConfig,InstitutionalConfig,RiskConfig,string,string)", + callers: [NORMAL, FAST_TRACK, CRITICAL], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "openVault(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "partialPauseVault(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "completePauseVault(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "unpauseVault(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "closeVault(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "sweep(address,address)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "approvePositionTransfer(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "revokePositionTransfer(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "setLiquidationThreshold(address,uint256)", + callers: [NORMAL, FAST_TRACK, CRITICAL], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "setLiquidationIncentive(address,uint256)", + callers: [NORMAL, FAST_TRACK, CRITICAL], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "setLatePenaltyRate(address,uint256)", + callers: [NORMAL, FAST_TRACK, CRITICAL], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + fn: "setVaultImplementation(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL], + }, + { target: INSTITUTIONAL_VAULT_CONTROLLER, fn: "setLiquidationAdapter(address)", callers: [NORMAL, GUARDIAN] }, + { target: INSTITUTIONAL_VAULT_CONTROLLER, fn: "setOracle(address)", callers: [NORMAL, GUARDIAN] }, + { target: INSTITUTIONAL_VAULT_CONTROLLER, fn: "setProtocolShareReserve(address)", callers: [NORMAL, GUARDIAN] }, + { target: INSTITUTIONAL_VAULT_CONTROLLER, fn: "setComptroller(address)", callers: [NORMAL, GUARDIAN] }, + { target: INSTITUTIONAL_VAULT_CONTROLLER, fn: "setTreasury(address)", callers: [NORMAL, GUARDIAN] }, + + // LiquidationAdapter + { + target: LIQUIDATION_ADAPTER, + fn: "setLiquidatorWhitelist(address,bool)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { + target: LIQUIDATION_ADAPTER, + fn: "setSettlerWhitelist(address,bool)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, + { target: LIQUIDATION_ADAPTER, fn: "setProtocolLiquidationShare(uint256)", callers: [NORMAL, FAST_TRACK, CRITICAL] }, + { target: LIQUIDATION_ADAPTER, fn: "setCloseFactor(uint256)", callers: [NORMAL, FAST_TRACK, CRITICAL] }, + { + target: LIQUIDATION_ADAPTER, + fn: "sweepProtocolShareToReserve(address)", + callers: [NORMAL, FAST_TRACK, CRITICAL, GUARDIAN], + }, +]; + +export const buildPermissions = (table: PermissionEntry[]): [string, string, string][] => + table.flatMap(({ target, fn, callers }) => callers.map(c => [target, fn, c] as [string, string, string])); + +export const PERMISSIONS: [string, string, string][] = buildPermissions(PERMISSION_ENTRIES); + +export const EXPECTED_PERMISSION_GRANTED_EVENTS = PERMISSIONS.length; + +export const vip664 = () => { + const meta = { + version: "v1", + title: "VIP-664 [BNB Chain] Configure Institutional Fixed Rate Vault System", + description: `#### Summary + +If passed, this VIP will configure the Institutional Fixed Rate Vault system on BNB Chain: + +1. Grant ACM permissions (${EXPECTED_PERMISSION_GRANTED_EVENTS} total) via \`ACMCommandsAggregator\` to the appropriate set of timelocks (Normal, Fast-track, Critical) and the Guardian for each access-controlled function on \`InstitutionalVaultController\` and \`LiquidationAdapter\`. +2. Accept ownership of \`InstitutionalVaultController\` and \`LiquidationAdapter\` (two-step Ownable2Step transfer initiated in deploy script). +3. Set the \`LiquidationAdapter\` on the controller via \`setLiquidationAdapter()\`. +4. Complete the two-step position token ownership transfer via \`acceptPositionTokenOwnership()\`. +5. Whitelist a dedicated set of liquidator and settler addresses on the \`LiquidationAdapter\`. + +#### Deployed Contracts + +- **InstitutionalVaultController** (proxy): ${INSTITUTIONAL_VAULT_CONTROLLER} +- **LiquidationAdapter** (proxy): ${LIQUIDATION_ADAPTER} +- **InstitutionPositionToken**: ${INSTITUTION_POSITION_TOKEN}`, + 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( + [ + // Phase 1 — Load and execute the ACM permission batch via the aggregator. + { + target: ACM_AGGREGATOR, + signature: "addGrantPermissions((address,string,address)[])", + params: [PERMISSIONS], + }, + { + target: ACCESS_CONTROL_MANAGER, + signature: "grantRole(bytes32,address)", + params: [DEFAULT_ADMIN_ROLE, ACM_AGGREGATOR], + }, + { + target: ACM_AGGREGATOR, + signature: "executeGrantPermissions(uint256)", + params: [ACM_AGGREGATOR_INDEX], + }, + { + target: ACCESS_CONTROL_MANAGER, + signature: "revokeRole(bytes32,address)", + params: [DEFAULT_ADMIN_ROLE, ACM_AGGREGATOR], + }, + + // Phase 2 — Complete Ownable2Step transfers (initiated in deploy script). + { target: INSTITUTIONAL_VAULT_CONTROLLER, signature: "acceptOwnership()", params: [] }, + { target: LIQUIDATION_ADAPTER, signature: "acceptOwnership()", params: [] }, + + // Phase 3 — Wire adapter into controller and accept position-token ownership. + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + signature: "setLiquidationAdapter(address)", + params: [LIQUIDATION_ADAPTER], + }, + { + target: INSTITUTIONAL_VAULT_CONTROLLER, + signature: "acceptPositionTokenOwnership()", + params: [], + }, + + // Phase 4 — Whitelist dedicated liquidator/settler addresses on the adapter. + ...LIQUIDATOR_WHITELIST.map(account => ({ + target: LIQUIDATION_ADAPTER, + signature: "setLiquidatorWhitelist(address,bool)", + params: [account, true], + })), + ...SETTLER_WHITELIST.map(account => ({ + target: LIQUIDATION_ADAPTER, + signature: "setSettlerWhitelist(address,bool)", + params: [account, true], + })), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip664;