diff --git a/simulations/vip-580/abi/ACM.json b/simulations/vip-580/abi/ACM.json new file mode 100644 index 000000000..2092ee121 --- /dev/null +++ b/simulations/vip-580/abi/ACM.json @@ -0,0 +1,81 @@ +[ + { + "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" + }, + { + "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" + } +] diff --git a/simulations/vip-580/abi/Comptroller.json b/simulations/vip-580/abi/Comptroller.json new file mode 100644 index 000000000..e55f5b415 --- /dev/null +++ b/simulations/vip-580/abi/Comptroller.json @@ -0,0 +1,209 @@ +[ + { + "inputs": [], + "name": "comptrollerImplementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "comptrollerLens", + "outputs": [ + { + "internalType": "contract ComptrollerLensInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationManager", + "outputs": [ + { + "internalType": "contract LiquidationManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "facetAddresses", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "facet", + "type": "address" + } + ], + "name": "facetFunctionSelectors", + "outputs": [ + { + "internalType": "bytes4[]", + "name": "", + "type": "bytes4[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "markets", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "", + "type": "uint96" + }, + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "facetAddress", + "type": "address" + }, + { + "internalType": "enum IDiamondCut.FacetCutAction", + "name": "action", + "type": "uint8" + }, + { + "internalType": "bytes4[]", + "name": "functionSelectors", + "type": "bytes4[]" + } + ], + "indexed": false, + "internalType": "struct IDiamondCut.FacetCut[]", + "name": "_diamondCut", + "type": "tuple[]" + } + ], + "name": "DiamondCut", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldImplementation", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "NewImplementation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldPendingImplementation", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newPendingImplementation", + "type": "address" + } + ], + "name": "NewPendingImplementation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldLiquidationManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newLiquidationManager", + "type": "address" + } + ], + "name": "NewLiquidationManager", + "type": "event" + } +] diff --git a/simulations/vip-580/abi/LiquidationManager.json b/simulations/vip-580/abi/LiquidationManager.json new file mode 100644 index 000000000..5ffb01dcc --- /dev/null +++ b/simulations/vip-580/abi/LiquidationManager.json @@ -0,0 +1,466 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "baseCloseFactorMantissa_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "defaultCloseFactorMantissa_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "targetHealthFactor_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "CollateralExceedsBorrowCapacity", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidBaseCloseFactor", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidDefaultCloseFactor", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTargetHealthFactor", + "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" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "DynamicCloseFactorEnabledSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "DynamicLiquidationIncentiveEnabledSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "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" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "baseCloseFactorMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint256", + "name": "borrowBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "wtAvgMantissa", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalCollateral", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dynamicLiquidationIncentive", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLiquidationIncentive", + "type": "uint256" + } + ], + "name": "calculateDynamicCloseFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "closeFactor", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint256", + "name": "healthFactor", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidationThresholdAvg", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLiquidationIncentiveMantissa", + "type": "uint256" + } + ], + "name": "calculateDynamicLiquidationIncentive", + "outputs": [ + { + "internalType": "uint256", + "name": "incentive", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultCloseFactorMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "dynamicCloseFactorEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "dynamicLiquidationIncentiveEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "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" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "setDynamicCloseFactorEnabled", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "setDynamicLiquidationIncentiveEnabled", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "targetHealthFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-580/abi/Liquidator.json b/simulations/vip-580/abi/Liquidator.json new file mode 100644 index 000000000..00dd8a8ad --- /dev/null +++ b/simulations/vip-580/abi/Liquidator.json @@ -0,0 +1,15 @@ +[ + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "implementation_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-580/abi/VAIController.json b/simulations/vip-580/abi/VAIController.json new file mode 100644 index 000000000..e3665dfb7 --- /dev/null +++ b/simulations/vip-580/abi/VAIController.json @@ -0,0 +1,53 @@ +[ + { + "inputs": [], + "name": "vaiControllerImplementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldImplementation", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "NewImplementation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldPendingImplementation", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newPendingImplementation", + "type": "address" + } + ], + "name": "NewPendingImplementation", + "type": "event" + } +] diff --git a/simulations/vip-580/abi/VBep20Delegate.json b/simulations/vip-580/abi/VBep20Delegate.json new file mode 100644 index 000000000..f5795fb5b --- /dev/null +++ b/simulations/vip-580/abi/VBep20Delegate.json @@ -0,0 +1,34 @@ +[ + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldImplementation", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "NewImplementation", + "type": "event" + } +] diff --git a/simulations/vip-580/bsctestnet.ts b/simulations/vip-580/bsctestnet.ts new file mode 100644 index 000000000..ac7c2184b --- /dev/null +++ b/simulations/vip-580/bsctestnet.ts @@ -0,0 +1,266 @@ +import { TransactionResponse } from "@ethersproject/providers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { Contract } from "ethers"; +import { parseUnits } from "ethers/lib/utils"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { expectEvents, initMainnetUser } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import { + LIQUIDATION_MANAGER, + LIQUIDATOR_PROXY_ADMIN, + NEW_COMPTROLLER_LENS, + NEW_DIAMOND, + NEW_LIQUIDATOR_IMPL, + NEW_VAI_CONTROLLER, + NEW_VTOKEN_IMPLEMENTATION, + vip580, +} from "../../vips/vip-580/bsctestnet"; +import ACM_ABI from "./abi/ACM.json"; +import COMPTROLLER_ABI from "./abi/Comptroller.json"; +import LIQUIDATOR_ABI from "./abi/Liquidator.json"; +import VAI_CONTROLLER_ABI from "./abi/VAIController.json"; +import VBEP20_DELEGATOR_ABI from "./abi/VBep20Delegate.json"; +import { cutParams as params } from "./utils/cut-params-bsctestnet.json"; +import CORE_POOL_VTOKENS from "./utils/market.json"; + +type CutParam = [string, number, string[]]; +const cutParams = params as unknown as CutParam[]; + +const { bsctestnet } = NETWORK_ADDRESSES; +const UNITROLLER = bsctestnet.UNITROLLER; +const ACM = bsctestnet.ACCESS_CONTROL_MANAGER; +const VAI_UNITROLLER = bsctestnet.VAI_UNITROLLER; +const LIQUIDATOR = bsctestnet.LIQUIDATOR; + +const OLD_DIAMOND = "0x1774f993861B14B7C3963F3e09f67cfBd2B32198"; +const OLD_COMPTROLLER_LENS = "0x72dCB93F8c3fB00D31076e93b6E87C342A3eCC9c"; +const OLD_VAI_CONTROLLER_IMPL = "0xA8122Fe0F9db39E266DE7A5BF953Cd72a87fe345"; +const OLD_LIQUIDATOR_IMPL = "0x91070E5b5Ff60a6c122740EB326D1f80E9f470e7"; +const OLD_VTOKEN_IMPL = "0xb941C5D148c65Ce49115D12B5148247AaCeFF375"; + +const OLD_SETTER_FACET = "0x4fc4C41388237D13A430879417f143EF54e5BB05"; +const OLD_REWARD_FACET = "0x0bc7922Cc08Ea32E196d25805558a84dF54beC6a"; +const OLD_MARKET_FACET = "0x8e0e15C99Ab0985cB39B2FE36532E5692730eBA9"; +const OLD_POLICY_FACET = "0xf8d94ef23c1188f8ab1009E56D558d7834d1F019"; +const OLD_FLASHLOAN_FACET = "0x32348c5bB52E5468A11901e70BdE061192feCAf4"; + +const NEW_SETTER_FACET = "0xe48f7E3F94349962A33D1e909b3F28E14A8770c9"; +const NEW_REWARD_FACET = "0x03Be0AAd2EADc48892335C6Ac10A71DaD5a81A15"; +const NEW_MARKET_FACET = "0x2A926859f87C322eEe043B8e5f098e618F92c529"; +const NEW_POLICY_FACET = "0x550F5408D34793a723C22ff84A6872d74D5597f1"; +const NEW_FLASHLOAN_FACET = "0x07347914d067C9227836870D6Be8F78539B91437"; + +const newMethods = [ + "setMarketMaxLiquidationIncentive(address,uint256)", + "setMarketMaxLiquidationIncentive(uint96,address,uint256)", + "setLiquidationManager(address)", + "setDynamicCloseFactorEnabled(address,bool)", + "setDynamicLiquidationIncentiveEnabled(address,bool)", +]; + +forking(76316411, async () => { + let unitroller: Contract; + let comptroller: Contract; + let accessControlManager: Contract; + let vaiUnitroller: Contract; + let liquidator: Contract; + let proxyAdmin: SignerWithAddress; + + before(async () => { + unitroller = await ethers.getContractAt(COMPTROLLER_ABI, UNITROLLER); + comptroller = await ethers.getContractAt(COMPTROLLER_ABI, UNITROLLER); + accessControlManager = await ethers.getContractAt(ACM_ABI, ACM); + vaiUnitroller = await ethers.getContractAt(VAI_CONTROLLER_ABI, VAI_UNITROLLER); + liquidator = await ethers.getContractAt(LIQUIDATOR_ABI, LIQUIDATOR); + proxyAdmin = await initMainnetUser(LIQUIDATOR_PROXY_ADMIN, parseUnits("2", 18)); + }); + + describe("Pre-VIP state", async () => { + it("unitroller should have old implementation", async () => { + const implementation = await unitroller.comptrollerImplementation(); + expect(implementation.toLowerCase()).to.equal(OLD_DIAMOND.toLowerCase()); + }); + + it("comptroller should have old comptroller lens", async () => { + const lens = await comptroller.comptrollerLens(); + expect(lens.toLowerCase()).to.equal(OLD_COMPTROLLER_LENS.toLowerCase()); + }); + + it("VAI Unitroller should point to old VAI Controller", async () => { + const implementation = await vaiUnitroller.vaiControllerImplementation(); + expect(implementation).to.equal(OLD_VAI_CONTROLLER_IMPL); + }); + + it("Liquidator should point to old implementation", async () => { + const impl = await liquidator.connect(proxyAdmin).callStatic.implementation(); + expect(impl.toLowerCase()).to.equal(OLD_LIQUIDATOR_IMPL.toLowerCase()); + }); + + it("vTokens should have old implementation", async () => { + const vToken = await ethers.getContractAt(VBEP20_DELEGATOR_ABI, CORE_POOL_VTOKENS[0].address); + const implementation = await vToken.implementation(); + expect(implementation.toLowerCase()).to.equal(OLD_VTOKEN_IMPL.toLowerCase()); + }); + + it("old liquidation incentive permission should exist", async () => { + const hasPermission = await accessControlManager.hasPermission( + bsctestnet.NORMAL_TIMELOCK, + UNITROLLER, + "setLiquidationIncentive(address,uint256)", + ); + expect(hasPermission).to.equal(true); + }); + + it("new liquidation functions should not have permissions", async () => { + for (const timelock of [ + bsctestnet.NORMAL_TIMELOCK, + bsctestnet.FAST_TRACK_TIMELOCK, + bsctestnet.CRITICAL_TIMELOCK, + bsctestnet.GUARDIAN, + ]) { + for (const method of newMethods) { + expect(await accessControlManager.hasPermission(timelock, UNITROLLER, method)).to.equal(false); + } + } + }); + }); + + testVip("VIP-580 Liquidation Threshold and Dynamic Liquidation Improvements", await vip580(), { + callbackAfterExecution: async (txResponse: TransactionResponse) => { + const totalMarkets = CORE_POOL_VTOKENS.length; + const totalTimelocks = 4; // Normal, Fast, Critical, Guardian + const newPermissionsPerTimelock = 5; // 5 new permissions + const oldPermissionsPerTimelock = 2; // 2 old permissions to revoke + + await expectEvents( + txResponse, + [COMPTROLLER_ABI, ACM_ABI], + ["NewPendingImplementation", "DiamondCut", "PermissionGranted", "PermissionRevoked"], + [ + 4, // Unitroller + VAI Controller (2x each for pending + become) + 1, // Diamond cut + newPermissionsPerTimelock * totalTimelocks, // New permissions + oldPermissionsPerTimelock * totalTimelocks, // Revoked permissions + ], + ); + + await expectEvents( + txResponse, + [VBEP20_DELEGATOR_ABI], + ["NewImplementation"], + [totalMarkets + 2], // All vTokens + Unitroller + VAI Controller + ); + }, + }); + + describe("Post-VIP state", async () => { + it("unitroller should have new implementation", async () => { + const implementation = await unitroller.comptrollerImplementation(); + expect(implementation).to.equal(NEW_DIAMOND); + }); + + it("comptroller should have new comptroller lens", async () => { + const lens = await comptroller.comptrollerLens(); + expect(lens).to.equal(NEW_COMPTROLLER_LENS); + }); + + it("VAI Controller should point to new implementation", async () => { + const implementation = await vaiUnitroller.vaiControllerImplementation(); + expect(implementation).to.equal(NEW_VAI_CONTROLLER); + }); + + it("Liquidator should point to new implementation", async () => { + const impl = await liquidator.connect(proxyAdmin).callStatic.implementation(); + expect(impl).to.equal(NEW_LIQUIDATOR_IMPL); + }); + + it("all vTokens should have new implementation", async () => { + for (const vToken of CORE_POOL_VTOKENS) { + const vTokenContract = await ethers.getContractAt(VBEP20_DELEGATOR_ABI, vToken.address); + const implementation = await vTokenContract.implementation(); + expect(implementation).to.equal(NEW_VTOKEN_IMPLEMENTATION); + } + }); + + it("liquidation manager should be set in comptroller", async () => { + const liquidationManagerAddress = await comptroller.liquidationManager(); + expect(liquidationManagerAddress).to.equal(LIQUIDATION_MANAGER); + }); + + it("market facet function selectors should be replaced with new facet address", async () => { + const functionSelectors = [...cutParams[0][2], ...cutParams[1][2]]; + expect(await unitroller.facetFunctionSelectors(NEW_MARKET_FACET)).to.deep.equal(functionSelectors); + expect(await unitroller.facetFunctionSelectors(OLD_MARKET_FACET)).to.deep.equal([]); + }); + + it("policy facet function selectors should be replaced with new facet address", async () => { + const functionSelectors = [...cutParams[2][2], ...cutParams[3][2]]; + expect(await unitroller.facetFunctionSelectors(NEW_POLICY_FACET)).to.deep.equal(functionSelectors); + expect(await unitroller.facetFunctionSelectors(OLD_POLICY_FACET)).to.deep.equal([]); + }); + + it("reward facet function selectors should be replaced with new facet address", async () => { + const functionSelectors = cutParams[4][2]; + expect(await unitroller.facetFunctionSelectors(NEW_REWARD_FACET)).to.deep.equal(functionSelectors); + expect(await unitroller.facetFunctionSelectors(OLD_REWARD_FACET)).to.deep.equal([]); + }); + + it("setter facet function selectors should be replaced with new facet address", async () => { + const functionSelectors = [...cutParams[5][2], ...cutParams[6][2]]; + expect(await unitroller.facetFunctionSelectors(NEW_SETTER_FACET)).to.deep.equal(functionSelectors); + expect(await unitroller.facetFunctionSelectors(OLD_SETTER_FACET)).to.deep.equal([]); + }); + + it("flashloan facet function selectors should be replaced with new facet address", async () => { + const functionSelectors = cutParams[7][2]; + expect(await unitroller.facetFunctionSelectors(NEW_FLASHLOAN_FACET)).to.deep.equal(functionSelectors); + expect(await unitroller.facetFunctionSelectors(OLD_FLASHLOAN_FACET)).to.deep.equal([]); + }); + + it("unitroller should contain the new facet addresses", async () => { + expect(await unitroller.facetAddresses()).to.include(NEW_SETTER_FACET, NEW_REWARD_FACET); + expect(await unitroller.facetAddresses()).to.include(NEW_MARKET_FACET, NEW_POLICY_FACET); + expect(await unitroller.facetAddresses()).to.include(NEW_FLASHLOAN_FACET); + + expect(await unitroller.facetAddresses()).to.not.include(OLD_SETTER_FACET, OLD_REWARD_FACET); + expect(await unitroller.facetAddresses()).to.not.include(OLD_MARKET_FACET, OLD_POLICY_FACET); + expect(await unitroller.facetAddresses()).to.not.include(OLD_FLASHLOAN_FACET); + }); + + it("old liquidation incentive permissions should be revoked", async () => { + for (const timelock of [ + bsctestnet.NORMAL_TIMELOCK, + bsctestnet.FAST_TRACK_TIMELOCK, + bsctestnet.CRITICAL_TIMELOCK, + bsctestnet.GUARDIAN, + ]) { + expect( + await accessControlManager.hasPermission(timelock, UNITROLLER, "setLiquidationIncentive(address,uint256)"), + ).to.equal(false); + expect( + await accessControlManager.hasPermission( + timelock, + UNITROLLER, + "setLiquidationIncentive(uint96,address,uint256)", + ), + ).to.equal(false); + } + }); + + it("new liquidation functions should have permissions", async () => { + for (const timelock of [ + bsctestnet.NORMAL_TIMELOCK, + bsctestnet.FAST_TRACK_TIMELOCK, + bsctestnet.CRITICAL_TIMELOCK, + bsctestnet.GUARDIAN, + ]) { + for (const method of newMethods) { + expect(await accessControlManager.hasPermission(timelock, UNITROLLER, method)).to.equal(true); + } + } + }); + }); +}); diff --git a/simulations/vip-580/utils/cut-params-bsctestnet.json b/simulations/vip-580/utils/cut-params-bsctestnet.json new file mode 100644 index 000000000..3b10091d6 --- /dev/null +++ b/simulations/vip-580/utils/cut-params-bsctestnet.json @@ -0,0 +1,139 @@ +{ + "cutParams": [ + [ + "0x2A926859f87C322eEe043B8e5f098e618F92c529", + 1, + [ + "0xa76b3fda", + "0x89c13be0", + "0x929fe9a1", + "0xd0d13036", + "0xc2998238", + "0xf9682732", + "0xede4edd0", + "0xb0772d0b", + "0xabfceffc", + "0x23617585", + "0xafd3783b", + "0x19ef3e8b", + "0xd686e9ee", + "0x7b86e42c", + "0x63e0d634", + "0xf02fdf97", + "0x007e3dd2", + "0x3d98a1e5", + "0x0ef332ca", + "0x8e8f294b", + "0x3093c11e", + "0xd137f36e", + "0xcab4f84c", + "0x0686dab6", + "0xddbf54fd" + ] + ], + [ + "0x2A926859f87C322eEe043B8e5f098e618F92c529", + 0, + ["0x1ec8ba02", "0xcd544f24", "0x9e9b1877", "0x13d97e2c", "0xe7ed111e", "0xa9e87456"] + ], + [ + "0x550F5408D34793a723C22ff84A6872d74D5597f1", + 1, + [ + "0xead1a8a0", + "0xda3d454c", + "0x5c778605", + "0x5ec88c79", + "0x528a174c", + "0x4e79238f", + "0x5fc7e71e", + "0x47ef3b3b", + "0x4ef4c3e1", + "0x41c728b9", + "0xeabe7d91", + "0x51dff989", + "0x24008a62", + "0x1ededc91", + "0xd02f7351", + "0x6d35bf91", + "0xbdcdc258", + "0x6a56947e" + ] + ], + ["0x550F5408D34793a723C22ff84A6872d74D5597f1", 0, ["0x14ecd653"]], + [ + "0x03Be0AAd2EADc48892335C6Ac10A71DaD5a81A15", + 1, + [ + "0xa7604b41", + "0xe85a2960", + "0x70bf66f0", + "0x86df31ee", + "0xadcd5fb9", + "0xd09c54ba", + "0x7858524d", + "0xbf32442d", + "0xededbae6", + "0x655f0725" + ] + ], + [ + "0xe48f7E3F94349962A33D1e909b3F28E14A8770c9", + 1, + [ + "0xf519fc30", + "0x2b5d790c", + "0x9bf34cbb", + "0x522c656b", + "0x17db2163", + "0xbb857450", + "0x607ef6c1", + "0x51a485e4", + "0x5f5af1aa", + "0x55ee1fe1", + "0x9460c8b5", + "0x2a6a6065", + "0xd24febad", + "0x9cfdd9e6", + "0x2ec04124", + "0x4e0853db", + "0x6662c7c9", + "0x919a3736", + "0x4ef233fc", + "0x24aaa220", + "0x7938146f", + "0x5cc4fdeb", + "0x9159b177", + "0xd6ad5c39", + "0x8b3113f6", + "0xa89766dd", + "0x186db48f", + "0xd136af44", + "0xfd51a3ad", + "0x4964f48c", + "0xb88d846b", + "0x530e784f", + "0xc32094c7", + "0x42adb211" + ] + ], + ["0xe48f7E3F94349962A33D1e909b3F28E14A8770c9", 0, ["0x38f92fc7", "0x75b74550", "0xc3b9d89a"]], + ["0x07347914d067C9227836870D6Be8F78539B91437", 1, ["0x5544ed9c"]], + [ + "0x0000000000000000000000000000000000000000", + 2, + [ + "0xc488847b", + "0xa78dc775", + "0xc5b4db55", + "0xd463654c", + "0x4d99c776", + "0xd585c3c6", + "0x317b0b77", + "0x12348e96", + "0x35439240", + "0x9bd8f6e8" + ] + ] + ] +} diff --git a/simulations/vip-580/utils/market.json b/simulations/vip-580/utils/market.json new file mode 100644 index 000000000..1421b0f76 --- /dev/null +++ b/simulations/vip-580/utils/market.json @@ -0,0 +1,154 @@ +[ + { + "symbol": "vUSDC", + "address": "0xD5C4C2e2facBEB59D0216D0595d63FcDc6F9A1a7" + }, + { + "symbol": "vUSDT", + "address": "0xb7526572FFE56AB9D7489838Bf2E18e3323b441A" + }, + { + "symbol": "vBUSD", + "address": "0x08e0A5575De71037aE36AbfAfb516595fE68e5e4" + }, + { + "symbol": "vSXP", + "address": "0x74469281310195A04840Daf6EdF576F559a3dE80" + }, + { + "symbol": "Venus XVS", + "address": "0x6d6F697e34145Bb95c54E77482d97cc261Dc237E" + }, + { + "symbol": "vETH", + "address": "0x162D005F0Fff510E54958Cfc5CF32A3180A84aab" + }, + { + "symbol": "vLTC", + "address": "0xAfc13BC065ABeE838540823431055D2ea52eBA52" + }, + { + "symbol": "vXRP", + "address": "0x488aB2826a154da01CC4CC16A8C83d4720D3cA2C" + }, + { + "symbol": "vBTC", + "address": "0xb6e9322C49FD75a367Fcb17B0Fcd62C5070EbCBe" + }, + { + "symbol": "vADA", + "address": "0x37C28DE42bA3d22217995D146FC684B2326Ede64" + }, + { + "symbol": "vDOGE", + "address": "0xF912d3001CAf6DC4ADD366A62Cc9115B4303c9A9" + }, + { + "symbol": "vCAKE", + "address": "0xeDaC03D29ff74b5fDc0CC936F6288312e1459BC6" + }, + { + "symbol": "vMATIC", + "address": "0x3619bdDc61189F33365CC572DF3a68FB3b316516" + }, + { + "symbol": "vAAVE", + "address": "0x714db6c38A17883964B68a07d56cE331501d9eb6" + }, + { + "symbol": "vTUSDOLD", + "address": "0x3A00d9B02781f47d033BAd62edc55fBF8D083Fb0" + }, + { + "symbol": "vTRXOLD", + "address": "0x369Fea97f6fB7510755DCA389088d9E2e2819278" + }, + { + "symbol": "vUST", + "address": "0xF206af85BC2761c4F876d27Bd474681CfB335EfA" + }, + { + "symbol": "vLUNA", + "address": "0x9C3015191d39cF1930F92EB7e7BCbd020bCA286a" + }, + { + "symbol": "vTRX", + "address": "0x6AF3Fdb3282c5bb6926269Db10837fa8Aec67C04" + }, + { + "symbol": "vWBETH", + "address": "0x35566ED3AF9E537Be487C98b1811cDf95ad0C32b" + }, + { + "symbol": "vTUSD", + "address": "0xEFAACF73CE2D38ED40991f29E72B12C74bd4cf23" + }, + { + "symbol": "vUNI", + "address": "0x171B468b52d7027F12cEF90cd065d6776a25E24e" + }, + { + "symbol": "vFDUSD", + "address": "0xF06e662a00796c122AaAE935EC4F0Be3F74f5636" + }, + { + "symbol": "vSolvBTC", + "address": "0xA38110ae4451A86ab754695057d5B5a9BEAd0387" + }, + { + "symbol": "vTWT", + "address": "0x95DaED37fdD3F557b3A5cCEb7D50Be65b36721DF" + }, + { + "symbol": "vTHE", + "address": "0x39A239F5117BFaC7a1b0b3A517c454113323451d" + }, + { + "symbol": "vSOL", + "address": "0xbd9EB061444665Df7282Ec0888b72D60aC41Eb8C" + }, + { + "symbol": "vlisUSD", + "address": "0x9447b1D4Bd192f25416B6aCc3B7f06be2f7D6309" + }, + { + "symbol": "vPT-sUSDE-26JUN2025", + "address": "0x90535B06ddB00453a5e5f2bC094d498F1cc86032" + }, + { + "symbol": "vsUSDe", + "address": "0x8c8A1a0b6e1cb8058037F7bF24de6b79Aca5B7B0" + }, + { + "symbol": "vUSDe", + "address": "0x86f8DfB7CA84455174EE9C3edd94867b51Da46BD" + }, + { + "symbol": "vUSD1", + "address": "0x519e61D2CDA04184FB086bbD2322C1bfEa0917Cf" + }, + { + "symbol": "vxSolvBTC", + "address": "0x97cB97B05697c377C0bd09feDce67DBd86B7aB1e" + }, + { + "symbol": "vasBNB", + "address": "0x73F506Aefd5e169D48Ea21A373B9B0a200E37585" + }, + { + "symbol": "vUSDF", + "address": "0x140d5Da2cE9fb9A8725cabdDB2Fe8ea831342C78" + }, + { + "symbol": "vWBNB", + "address": "0xd9E77847ec815E56ae2B9E69596C69b6972b0B1C" + }, + { + "symbol": "vPT-USDe-30OCT2025", + "address": "0x86a94290f2B8295daA3e53bA1286f2Ff21199143" + }, + { + "symbol": "vslisBNB", + "address": "0xaB5504A3cde0d8253E8F981D663c7Ff7128B3e56" + } +] diff --git a/src/networkAddresses.ts b/src/networkAddresses.ts index 757be8868..c9c45548e 100644 --- a/src/networkAddresses.ts +++ b/src/networkAddresses.ts @@ -75,6 +75,7 @@ export const NETWORK_ADDRESSES = { BINANCE_ORACLE: oracleBsctestnetContracts.addresses.BinanceOracle, RESILIENT_ORACLE: oracleBsctestnetContracts.addresses.ResilientOracle, REDSTONE_ORACLE: oracleBsctestnetContracts.addresses.RedStoneOracle, + LIQUIDATOR: bsctestnetDeployedContracts.addresses.Liquidator, }, ethereum: { NORMAL_TIMELOCK: "0xd969E79406c35E80750aAae061D402Aab9325714", diff --git a/vips/vip-580/bsctestnet.ts b/vips/vip-580/bsctestnet.ts new file mode 100644 index 000000000..afa0a0d4f --- /dev/null +++ b/vips/vip-580/bsctestnet.ts @@ -0,0 +1,195 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { cutParams } from "../../simulations/vip-580/utils/cut-params-bsctestnet.json"; +import CORE_POOL_VTOKENS from "../../simulations/vip-580/utils/market.json"; + +const { bsctestnet } = NETWORK_ADDRESSES; + +const ACM = bsctestnet.ACCESS_CONTROL_MANAGER; +const LIQUIDATOR = bsctestnet.LIQUIDATOR; +const UNITROLLER = bsctestnet.UNITROLLER; +const VAI_UNITROLLER = bsctestnet.VAI_UNITROLLER; + +const NORMAL_TIMELOCK = bsctestnet.NORMAL_TIMELOCK; +const FAST_TRACK_TIMELOCK = bsctestnet.FAST_TRACK_TIMELOCK; +const CRITICAL_TIMELOCK = bsctestnet.CRITICAL_TIMELOCK; + +export const LIQUIDATOR_PROXY_ADMIN = "0x1469AeB2768931f979a1c957692e32Aa802dd55a"; +export const LIQUIDATION_MANAGER = "0x03CF41c8777A4e359147309F74a53c8b6b4c6969"; +export const NEW_COMPTROLLER_LENS = "0x9D542132fa552B6b416944501bF0D689286E1535"; +export const NEW_DIAMOND = "0xe492CCD207760fE4Dfa9B83Fd1590632dDE33BAB"; +export const NEW_LIQUIDATOR_IMPL = "0xbC89aA9ab926b8491CB7E613A56A13A14BCfa8bc"; +export const NEW_VAI_CONTROLLER = "0xBc98737283f10Dd759DB79cD5f8d11Da909D992b"; +export const NEW_VTOKEN_IMPLEMENTATION = "0xC0c413F41281C61E160c47FCC215D9d0BC6AC72d"; + +const PAUSE_GUARDIAN_MULTISIG = bsctestnet.GUARDIAN; + +interface AccessControl { + target: string; + signature: string; + params: Array; +} + +const revokeAccessControl = () => { + const accessProposals: Array = []; + [NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, PAUSE_GUARDIAN_MULTISIG].map(target => { + accessProposals.push({ + target: ACM, + signature: "revokeCallPermission(address,string,address)", + params: [UNITROLLER, "setLiquidationIncentive(address,uint256)", target], + }); + accessProposals.push({ + target: ACM, + signature: "revokeCallPermission(address,string,address)", + params: [UNITROLLER, "setLiquidationIncentive(uint96,address,uint256)", target], + }); + }); + + return accessProposals; +}; + +const grantAccessControl = () => { + const accessProposals: Array = []; + [NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK, PAUSE_GUARDIAN_MULTISIG].map(target => { + accessProposals.push({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [UNITROLLER, "setMarketMaxLiquidationIncentive(address,uint256)", target], + }); + accessProposals.push({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [UNITROLLER, "setMarketMaxLiquidationIncentive(uint96,address,uint256)", target], + }); + accessProposals.push({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [UNITROLLER, "setLiquidationManager(address)", target], + }); + accessProposals.push({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [UNITROLLER, "setDynamicCloseFactorEnabled(address,bool)", target], + }); + accessProposals.push({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [UNITROLLER, "setDynamicLiquidationIncentiveEnabled(address,bool)", target], + }); + }); + + return accessProposals; +}; + +export const vip580 = () => { + const meta = { + version: "v2", + title: "VIP-580 Liquidation improvements for Core Pool on BNB Chain Testnet", + description: `#### Summary + +This VIP introduces enhanced liquidation mechanisms to the Venus Core Pool on BNB Chain Testnet, implementing the following improvements: + +#### Description + +- **Liquidation Threshold**: Adds the concept of liquidation threshold to the Core Pool, enabling more granular control over when positions become eligible for liquidation +- **Dynamic Liquidation Incentive**: Implements dynamic liquidation incentives that adjust based on market conditions and users account health factors +- **Dynamic Close Factor**: Introduces dynamic close factors that vary based on borrowers health factor, allowing for more flexible liquidation amounts +- **Maximum Liquidation Incentive**: Defines maximum liquidation incentive per seized asset to protect borrowers from excessive liquidation penalties + +#### Contract Upgrades + +This VIP will perform the following: + +- Upgrade Comptroller (Unitroller) to new Diamond implementation with updated facets +- Deploy and configure the new Liquidation Manager contract +- Upgrade VAI Controller with updated liquidation logic +- Upgrade Liquidator contract to support dynamic incentives +- Upgrade all Core Pool vTokens to new implementation +- Update Comptroller Lens to reflect new liquidation parameters +- Configure new access controls for liquidation parameters + +#### References + +- [VIP Pull Request](https://github.com/VenusProtocol/venus-protocol/pull/604) +- [Community Forum Discussion](https://community.venus.io) +`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + // Set new implementation for unitroller + { + target: UNITROLLER, + signature: "_setPendingImplementation(address)", + params: [NEW_DIAMOND], + }, + { + target: NEW_DIAMOND, + signature: "_become(address)", + params: [UNITROLLER], + }, + + // Configure facets and functions selectors + { + target: UNITROLLER, + signature: "diamondCut((address,uint8,bytes4[])[])", + params: [cutParams], + }, + + // Grant access to the new functions + ...grantAccessControl(), + + // Set liquidation manager in comptroller + { + target: UNITROLLER, + signature: "setLiquidationManager(address)", + params: [LIQUIDATION_MANAGER], + }, + + // Set new implementation for comptroller lens + { + target: UNITROLLER, + signature: "_setComptrollerLens(address)", + params: [NEW_COMPTROLLER_LENS], + }, + + // Set new implementation for vai controller + { + target: VAI_UNITROLLER, + signature: "_setPendingImplementation(address)", + params: [NEW_VAI_CONTROLLER], + }, + { + target: NEW_VAI_CONTROLLER, + signature: "_become(address)", + params: [VAI_UNITROLLER], + }, + + // Set new implementation for liquidator + { + target: LIQUIDATOR_PROXY_ADMIN, + signature: "upgrade(address,address)", + params: [LIQUIDATOR, NEW_LIQUIDATOR_IMPL], + }, + + // Set new implementation for core pool vtokens + ...CORE_POOL_VTOKENS.map(vToken => ({ + target: vToken.address, + signature: "_setImplementation(address,bool,bytes)", + params: [NEW_VTOKEN_IMPLEMENTATION, false, "0x"], + })), + + // Revoke access to the removed functions + ...revokeAccessControl(), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip580;