From dcde196d14eac1d1fa55a1fbe58d2e43ab27f13a Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 27 Apr 2026 13:17:18 +0530 Subject: [PATCH 01/15] feat: add VIP-666 to configure DeviationSentinel + EBrakeV2 - Wires DeviationSentinel + EBrakeV2 + SentinelOracle + UniswapOracle on Ethereum, Arbitrum One, zkSync Era, and Base - Grants only the IL-supported subset of EBrake action functions since these chains use isIsolatedPool=true (Diamond-only fns would revert) - Add ACCESS_CONTROL_MANAGER for arbitrumone, zksyncmainnet, and basemainnet to networkAddresses.ts - Include cross-chain simulation suite with pre/post permission, ownership, and functional sanity checks; auto-skips when placeholder addresses are unfilled --- .../vip-666/abi/AccessControlManager.json | 360 +++ .../vip-666/abi/DeviationSentinel.json | 589 +++++ simulations/vip-666/abi/EBrake.json | 683 ++++++ simulations/vip-666/abi/ILComptroller.json | 2121 +++++++++++++++++ simulations/vip-666/abi/SentinelOracle.json | 331 +++ simulations/vip-666/abi/UniswapOracle.json | 299 +++ simulations/vip-666/arbitrumone.ts | 11 + simulations/vip-666/basemainnet.ts | 11 + simulations/vip-666/ethereum.ts | 11 + simulations/vip-666/shared.ts | 366 +++ simulations/vip-666/zksyncmainnet.ts | 11 + src/networkAddresses.ts | 3 + vips/vip-666/addresses/arbitrumone.ts | 47 + vips/vip-666/addresses/basemainnet.ts | 47 + vips/vip-666/addresses/ethereum.ts | 47 + vips/vip-666/addresses/zksyncmainnet.ts | 47 + vips/vip-666/bscmainnet.ts | 225 ++ 17 files changed, 5209 insertions(+) create mode 100644 simulations/vip-666/abi/AccessControlManager.json create mode 100644 simulations/vip-666/abi/DeviationSentinel.json create mode 100644 simulations/vip-666/abi/EBrake.json create mode 100644 simulations/vip-666/abi/ILComptroller.json create mode 100644 simulations/vip-666/abi/SentinelOracle.json create mode 100644 simulations/vip-666/abi/UniswapOracle.json create mode 100644 simulations/vip-666/arbitrumone.ts create mode 100644 simulations/vip-666/basemainnet.ts create mode 100644 simulations/vip-666/ethereum.ts create mode 100644 simulations/vip-666/shared.ts create mode 100644 simulations/vip-666/zksyncmainnet.ts create mode 100644 vips/vip-666/addresses/arbitrumone.ts create mode 100644 vips/vip-666/addresses/basemainnet.ts create mode 100644 vips/vip-666/addresses/ethereum.ts create mode 100644 vips/vip-666/addresses/zksyncmainnet.ts create mode 100644 vips/vip-666/bscmainnet.ts diff --git a/simulations/vip-666/abi/AccessControlManager.json b/simulations/vip-666/abi/AccessControlManager.json new file mode 100644 index 000000000..4a118fcc4 --- /dev/null +++ b/simulations/vip-666/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-666/abi/DeviationSentinel.json b/simulations/vip-666/abi/DeviationSentinel.json new file mode 100644 index 000000000..c12ea017a --- /dev/null +++ b/simulations/vip-666/abi/DeviationSentinel.json @@ -0,0 +1,589 @@ +[ + { + "inputs": [ + { + "internalType": "contract IEBrake", + "name": "eBrake_", + "type": "address" + }, + { + "internalType": "contract ResilientOracleInterface", + "name": "resilientOracle_", + "type": "address" + }, + { + "internalType": "contract OracleInterface", + "name": "sentinelOracle_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "ExceedsMaxDeviation", + "type": "error" + }, + { + "inputs": [], + "name": "MarketNotConfigured", + "type": "error" + }, + { + "inputs": [], + "name": "TokenMonitoringDisabled", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "UnauthorizedKeeper", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroDeviation", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "BorrowPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "MarketStateReset", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAccessControlManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAccessControlManager", + "type": "address" + } + ], + "name": "NewAccessControlManager", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "SupplyPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "deviation", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct DeviationSentinel.DeviationConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "TokenConfigUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "TokenMonitoringStatusChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "keeper", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isTrusted", + "type": "bool" + } + ], + "name": "TrustedKeeperUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "EBRAKE", + "outputs": [ + { + "internalType": "contract IEBrake", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_DEVIATION", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "RESILIENT_ORACLE", + "outputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SENTINEL_ORACLE", + "outputs": [ + { + "internalType": "contract OracleInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IVToken", + "name": "market", + "type": "address" + } + ], + "name": "checkPriceDeviation", + "outputs": [ + { + "internalType": "bool", + "name": "hasDeviation", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "oraclePrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sentinelPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deviationPercent", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IVToken", + "name": "market", + "type": "address" + } + ], + "name": "handleDeviation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "marketStates", + "outputs": [ + { + "internalType": "bool", + "name": "borrowPaused", + "type": "bool" + }, + { + "internalType": "bool", + "name": "cfModifiedAndSupplyPaused", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IVToken", + "name": "market", + "type": "address" + } + ], + "name": "resetMarketState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "setAccessControlManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "deviation", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "internalType": "struct DeviationSentinel.DeviationConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "setTokenConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "setTokenMonitoringEnabled", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "keeper", + "type": "address" + }, + { + "internalType": "bool", + "name": "isTrusted", + "type": "bool" + } + ], + "name": "setTrustedKeeper", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "tokenConfigs", + "outputs": [ + { + "internalType": "uint8", + "name": "deviation", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "trustedKeepers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-666/abi/EBrake.json b/simulations/vip-666/abi/EBrake.json new file mode 100644 index 000000000..c7aedf09d --- /dev/null +++ b/simulations/vip-666/abi/EBrake.json @@ -0,0 +1,683 @@ +[ + { + "inputs": [ + { + "internalType": "contract ICorePoolComptroller", + "name": "corePoolComptroller_", + "type": "address" + }, + { + "internalType": "bool", + "name": "isIsolatedPool_", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "ArrayLengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint256", + "name": "currentCap", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requestedCap", + "type": "uint256" + } + ], + "name": "CapExceedsCurrent", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyArray", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum IComptroller.Action", + "name": "action", + "type": "uint8" + } + ], + "name": "ForbiddenAction", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "MarketNotListed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorCode", + "type": "uint256" + } + ], + "name": "SetCollateralFactorFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum IComptroller.Action", + "name": "action", + "type": "uint8" + } + ], + "name": "ActionPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "enum IComptroller.Action[]", + "name": "actions", + "type": "uint8[]" + } + ], + "name": "ActionsPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "newBorrowCaps", + "type": "uint256[]" + } + ], + "name": "BorrowCapsDecreased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "CollateralFactorZeroed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + } + ], + "name": "FlashLoanPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "MarketStateReset", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAccessControlManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAccessControlManager", + "type": "address" + } + ], + "name": "NewAccessControlManager", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "newSupplyCaps", + "type": "uint256[]" + } + ], + "name": "SupplyCapsDecreased", + "type": "event" + }, + { + "inputs": [], + "name": "COMPTROLLER", + "outputs": [ + { + "internalType": "contract ICorePoolComptroller", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "IS_ISOLATED_POOL", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "getMarketCFSnapshot", + "outputs": [ + { + "internalType": "uint256", + "name": "cf", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lt", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "marketStates", + "outputs": [ + { + "internalType": "uint256", + "name": "borrowCap", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "supplyCap", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "borrowCapSnapshotted", + "type": "bool" + }, + { + "internalType": "bool", + "name": "supplyCapSnapshotted", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "internalType": "enum IComptroller.Action[]", + "name": "actions", + "type": "uint8[]" + } + ], + "name": "pauseActions", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "pauseBorrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pauseFlashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "pauseRedeem", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "pauseSupply", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "pauseTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "resetMarketState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "setAccessControlManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "setCFZero", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "setCFZero", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newBorrowCaps", + "type": "uint256[]" + } + ], + "name": "setMarketBorrowCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "markets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newSupplyCaps", + "type": "uint256[]" + } + ], + "name": "setMarketSupplyCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-666/abi/ILComptroller.json b/simulations/vip-666/abi/ILComptroller.json new file mode 100644 index 000000000..d1801efd8 --- /dev/null +++ b/simulations/vip-666/abi/ILComptroller.json @@ -0,0 +1,2121 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "poolRegistry_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "enum Action", + "name": "action", + "type": "uint8" + } + ], + "name": "ActionPaused", + "type": "error" + }, + { + "inputs": [], + "name": "BorrowActionNotPaused", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint256", + "name": "cap", + "type": "uint256" + } + ], + "name": "BorrowCapExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "BorrowCapIsNotZero", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expectedLessThanOrEqualTo", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "CollateralExceedsThreshold", + "type": "error" + }, + { + "inputs": [], + "name": "CollateralFactorIsNotZero", + "type": "error" + }, + { + "inputs": [], + "name": "ComptrollerMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "DelegationStatusUnchanged", + "type": "error" + }, + { + "inputs": [], + "name": "EnterMarketActionNotPaused", + "type": "error" + }, + { + "inputs": [], + "name": "ExitMarketActionNotPaused", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "collateralToSeize", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "availableCollateral", + "type": "uint256" + } + ], + "name": "InsufficientCollateral", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientLiquidity", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientShortfall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCollateralFactor", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidLiquidationThreshold", + "type": "error" + }, + { + "inputs": [], + "name": "LiquidateActionNotPaused", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "MarketAlreadyListed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "MarketNotCollateral", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "MarketNotListed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "loopsLimit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requiredLoops", + "type": "uint256" + } + ], + "name": "MaxLoopsLimitExceeded", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expectedGreaterThan", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "MinimalCollateralViolated", + "type": "error" + }, + { + "inputs": [], + "name": "MintActionNotPaused", + "type": "error" + }, + { + "inputs": [], + "name": "NonzeroBorrowBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "PriceError", + "type": "error" + }, + { + "inputs": [], + "name": "RedeemActionNotPaused", + "type": "error" + }, + { + "inputs": [], + "name": "RepayActionNotPaused", + "type": "error" + }, + { + "inputs": [], + "name": "SeizeActionNotPaused", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "SnapshotError", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint256", + "name": "cap", + "type": "uint256" + } + ], + "name": "SupplyCapExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "SupplyCapIsNotZero", + "type": "error" + }, + { + "inputs": [], + "name": "TooMuchRepay", + "type": "error" + }, + { + "inputs": [], + "name": "TransferActionNotPaused", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "expectedSender", + "type": "address" + }, + { + "internalType": "address", + "name": "actualSender", + "type": "address" + } + ], + "name": "UnexpectedSender", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum Action", + "name": "action", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bool", + "name": "pauseState", + "type": "bool" + } + ], + "name": "ActionPausedMarket", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "approver", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "DelegateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "IsForcedLiquidationEnabledUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "MarketEntered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "MarketExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "MarketSupported", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "MarketUnlisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldMaxLoopsLimit", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newmaxLoopsLimit", + "type": "uint256" + } + ], + "name": "MaxLoopsLimitUpdated", + "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": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBorrowCap", + "type": "uint256" + } + ], + "name": "NewBorrowCap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldCloseFactorMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newCloseFactorMantissa", + "type": "uint256" + } + ], + "name": "NewCloseFactor", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldCollateralFactorMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newCollateralFactorMantissa", + "type": "uint256" + } + ], + "name": "NewCollateralFactor", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldLiquidationIncentiveMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLiquidationIncentiveMantissa", + "type": "uint256" + } + ], + "name": "NewLiquidationIncentive", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldLiquidationThresholdMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLiquidationThresholdMantissa", + "type": "uint256" + } + ], + "name": "NewLiquidationThreshold", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldMinLiquidatableCollateral", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newMinLiquidatableCollateral", + "type": "uint256" + } + ], + "name": "NewMinLiquidatableCollateral", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract ResilientOracleInterface", + "name": "oldPriceOracle", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract ResilientOracleInterface", + "name": "newPriceOracle", + "type": "address" + } + ], + "name": "NewPriceOracle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract IPrime", + "name": "oldPrimeToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IPrime", + "name": "newPrimeToken", + "type": "address" + } + ], + "name": "NewPrimeToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "rewardsDistributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "rewardToken", + "type": "address" + } + ], + "name": "NewRewardsDistributor", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newSupplyCap", + "type": "uint256" + } + ], + "name": "NewSupplyCap", + "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": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "accountAssets", + "outputs": [ + { + "internalType": "contract VToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "enum Action", + "name": "action", + "type": "uint8" + } + ], + "name": "actionPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract RewardsDistributor", + "name": "_rewardsDistributor", + "type": "address" + } + ], + "name": "addRewardsDistributor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allMarkets", + "outputs": [ + { + "internalType": "contract VToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "approvedDelegates", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "borrowCaps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "borrowVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "checkMembership", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "closeFactorMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "vTokens", + "type": "address[]" + } + ], + "name": "enterMarkets", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenAddress", + "type": "address" + } + ], + "name": "exitMarket", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "error", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shortfall", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllMarkets", + "outputs": [ + { + "internalType": "contract VToken[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAssetsIn", + "outputs": [ + { + "internalType": "contract VToken[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getBorrowingPower", + "outputs": [ + { + "internalType": "uint256", + "name": "error", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shortfall", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenModify", + "type": "address" + }, + { + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "getHypotheticalAccountLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "error", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shortfall", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRewardDistributors", + "outputs": [ + { + "internalType": "contract RewardsDistributor[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "getRewardsByMarket", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rewardToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "supplySpeed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowSpeed", + "type": "uint256" + } + ], + "internalType": "struct ComptrollerStorage.RewardSpeeds[]", + "name": "rewardSpeeds", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "healAccount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "loopLimit", + "type": "uint256" + }, + { + "internalType": "address", + "name": "accessControlManager", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "isComptroller", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isForcedLiquidationEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "isMarketListed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract VToken", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "contract VToken", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + } + ], + "internalType": "struct ComptrollerStorage.LiquidationOrder[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "liquidateAccount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualRepayAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "liquidateBorrowVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualRepayAmount", + "type": "uint256" + } + ], + "name": "liquidateCalculateSeizeTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "error", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokensToSeize", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationIncentiveMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "markets", + "outputs": [ + { + "internalType": "bool", + "name": "isListed", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "collateralFactorMantissa", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidationThresholdMantissa", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxLoopsLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minLiquidatableCollateral", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualMintAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "mintTokens", + "type": "uint256" + } + ], + "name": "mintVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "oracle", + "outputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolRegistry", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "preBorrowHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "skipLiquidityCheck", + "type": "bool" + } + ], + "name": "preLiquidateHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + } + ], + "name": "preMintHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + } + ], + "name": "preRedeemHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + } + ], + "name": "preRepayHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "address", + "name": "seizerContract", + "type": "address" + }, + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + } + ], + "name": "preSeizeHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "transferTokens", + "type": "uint256" + } + ], + "name": "preTransferHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "prime", + "outputs": [ + { + "internalType": "contract IPrime", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "redeemAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + } + ], + "name": "redeemVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualRepayAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowerIndex", + "type": "uint256" + } + ], + "name": "repayBorrowVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "seizeVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "setAccessControlManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken[]", + "name": "marketsList", + "type": "address[]" + }, + { + "internalType": "enum Action[]", + "name": "actionsList", + "type": "uint8[]" + }, + { + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "setActionsPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newCloseFactorMantissa", + "type": "uint256" + } + ], + "name": "setCloseFactor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "newCollateralFactorMantissa", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newLiquidationThresholdMantissa", + "type": "uint256" + } + ], + "name": "setCollateralFactor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "setForcedLiquidation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newLiquidationIncentiveMantissa", + "type": "uint256" + } + ], + "name": "setLiquidationIncentive", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newBorrowCaps", + "type": "uint256[]" + } + ], + "name": "setMarketBorrowCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newSupplyCaps", + "type": "uint256[]" + } + ], + "name": "setMarketSupplyCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "limit", + "type": "uint256" + } + ], + "name": "setMaxLoopsLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMinLiquidatableCollateral", + "type": "uint256" + } + ], + "name": "setMinLiquidatableCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "newOracle", + "type": "address" + } + ], + "name": "setPriceOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPrime", + "name": "_prime", + "type": "address" + } + ], + "name": "setPrimeToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "supplyCaps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "supportMarket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "transferTokens", + "type": "uint256" + } + ], + "name": "transferVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "unlistMarket", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "updateDelegate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "updatePrices", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-666/abi/SentinelOracle.json b/simulations/vip-666/abi/SentinelOracle.json new file mode 100644 index 000000000..7c9930f95 --- /dev/null +++ b/simulations/vip-666/abi/SentinelOracle.json @@ -0,0 +1,331 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "TokenNotConfigured", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "DirectPriceUpdated", + "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" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "oracle", + "type": "address" + } + ], + "name": "TokenOracleConfigUpdated", + "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": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "directPrices", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "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": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "setDirectPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + } + ], + "name": "setTokenOracleConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "tokenConfigs", + "outputs": [ + { + "internalType": "address", + "name": "oracle", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-666/abi/UniswapOracle.json b/simulations/vip-666/abi/UniswapOracle.json new file mode 100644 index 000000000..66770c861 --- /dev/null +++ b/simulations/vip-666/abi/UniswapOracle.json @@ -0,0 +1,299 @@ +[ + { + "inputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "resilientOracle_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "InvalidPool", + "type": "error" + }, + { + "inputs": [], + "name": "TokenNotConfigured", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": 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" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolConfigUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "RESILIENT_ORACLE", + "outputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "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": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "setPoolConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "tokenPools", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-666/arbitrumone.ts b/simulations/vip-666/arbitrumone.ts new file mode 100644 index 000000000..18b28ef84 --- /dev/null +++ b/simulations/vip-666/arbitrumone.ts @@ -0,0 +1,11 @@ +import { forking } from "src/vip-framework"; + +import { ARBITRUMONE_CONFIG } from "../../vips/vip-666/addresses/arbitrumone"; +import { runVip666Suite } from "./shared"; + +// TODO: set fork block once the EBrake/DeviationSentinel stack is deployed on Arbitrum One. +const FORK_BLOCK = 0; + +forking(FORK_BLOCK, async () => { + await runVip666Suite(ARBITRUMONE_CONFIG); +}); diff --git a/simulations/vip-666/basemainnet.ts b/simulations/vip-666/basemainnet.ts new file mode 100644 index 000000000..f877da806 --- /dev/null +++ b/simulations/vip-666/basemainnet.ts @@ -0,0 +1,11 @@ +import { forking } from "src/vip-framework"; + +import { BASEMAINNET_CONFIG } from "../../vips/vip-666/addresses/basemainnet"; +import { runVip666Suite } from "./shared"; + +// TODO: set fork block once the EBrake/DeviationSentinel stack is deployed on Base. +const FORK_BLOCK = 0; + +forking(FORK_BLOCK, async () => { + await runVip666Suite(BASEMAINNET_CONFIG); +}); diff --git a/simulations/vip-666/ethereum.ts b/simulations/vip-666/ethereum.ts new file mode 100644 index 000000000..6b9c591fb --- /dev/null +++ b/simulations/vip-666/ethereum.ts @@ -0,0 +1,11 @@ +import { forking } from "src/vip-framework"; + +import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; +import { runVip666Suite } from "./shared"; + +// TODO: set fork block once the EBrake/DeviationSentinel stack is deployed on Ethereum. +const FORK_BLOCK = 0; + +forking(FORK_BLOCK, async () => { + await runVip666Suite(ETHEREUM_CONFIG); +}); diff --git a/simulations/vip-666/shared.ts b/simulations/vip-666/shared.ts new file mode 100644 index 000000000..759233714 --- /dev/null +++ b/simulations/vip-666/shared.ts @@ -0,0 +1,366 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { ZERO_ADDRESS } from "src/networkAddresses"; +import { expectEvents, initMainnetUser } from "src/utils"; +import { testForkedNetworkVipCommands } from "src/vip-framework"; + +import vip666, { + ChainConfig, + DIAMOND_ONLY_EBRAKE_PERMS, + EBRAKE_COMPTROLLER_PERMS_IL, + GOVERNANCE_EBRAKE_PERMS_IL, + RESET_PERMS, + SENTINEL_ADMIN_PERMS, + SENTINEL_EBRAKE_PERMS, + SENTINEL_ORACLE_ADMIN_PERMS, + UNISWAP_ORACLE_ADMIN_PERMS, +} from "../../vips/vip-666/bscmainnet"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; +import DEVIATION_SENTINEL_ABI from "./abi/DeviationSentinel.json"; +import EBRAKE_ABI from "./abi/EBrake.json"; +import SENTINEL_ORACLE_ABI from "./abi/SentinelOracle.json"; +import UNISWAP_ORACLE_ABI from "./abi/UniswapOracle.json"; + +// Total RoleGranted events per chain when no token wiring runs: +// 3 (sentinel admin) × 4 + 2 (sentinel oracle admin) × 4 + 1 (uniswap oracle admin) × 4 +// + 4 (ebrake → comptroller) + 3 (reset) × 4 + 3 (sentinel → ebrake) +// + 8 (governance ebrake action) × 4 + 8 (multisig ebrake action) +// = 12 + 8 + 4 + 4 + 12 + 3 + 32 + 8 = 83 +const PERMS_GRANTED_PER_CHAIN = 83; + +const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { + const missing: string[] = []; + if (cfg.deviationSentinel === ZERO_ADDRESS) missing.push("deviationSentinel"); + if (cfg.eBrake === ZERO_ADDRESS) missing.push("eBrake"); + if (cfg.sentinelOracle === ZERO_ADDRESS) missing.push("sentinelOracle"); + if (cfg.uniswapOracle === ZERO_ADDRESS) missing.push("uniswapOracle"); + if (cfg.multisigPauser === ZERO_ADDRESS) missing.push("multisigPauser"); + if (cfg.keeper === ZERO_ADDRESS) missing.push("keeper"); + return missing; +}; + +export const runVip666Suite = async (cfg: ChainConfig) => { + const missing = collectMissingPlaceholders(cfg); + if (missing.length > 0) { + describe.skip(`VIP-666 [${cfg.name}] — placeholder addresses missing: ${missing.join(", ")}`, () => { + it(`Fill ${missing.join(", ")} in addresses/${cfg.name + .toLowerCase() + .replace(/\s/g, "")}.ts to run this suite`, () => { + // intentionally empty — skip stub + }); + }); + return; + } + + let acm: Contract; + let deviationSentinel: Contract; + let eBrake: Contract; + let sentinelOracle: Contract; + let uniswapOracle: Contract; + + // ACM.isAllowedToCall(account, sig) checks msg.sender as the host contract. + // Impersonate each host so the role lookup resolves correctly. + let impersonatedDeviationSentinel: SignerWithAddress; + let impersonatedSentinelOracle: SignerWithAddress; + let impersonatedUniswapOracle: SignerWithAddress; + let impersonatedEBrake: SignerWithAddress; + let impersonatedComptroller: SignerWithAddress; + + const governanceAccounts = [cfg.guardian, cfg.normalTimelock, cfg.fastTrackTimelock, cfg.criticalTimelock]; + const trustedKeeperAccounts = [cfg.keeper, ...governanceAccounts]; + + before(async () => { + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, cfg.acm); + deviationSentinel = await ethers.getContractAt(DEVIATION_SENTINEL_ABI, cfg.deviationSentinel); + eBrake = await ethers.getContractAt(EBRAKE_ABI, cfg.eBrake); + sentinelOracle = await ethers.getContractAt(SENTINEL_ORACLE_ABI, cfg.sentinelOracle); + uniswapOracle = await ethers.getContractAt(UNISWAP_ORACLE_ABI, cfg.uniswapOracle); + + impersonatedDeviationSentinel = await initMainnetUser(cfg.deviationSentinel, ethers.utils.parseEther("1")); + impersonatedSentinelOracle = await initMainnetUser(cfg.sentinelOracle, ethers.utils.parseEther("1")); + impersonatedUniswapOracle = await initMainnetUser(cfg.uniswapOracle, ethers.utils.parseEther("1")); + impersonatedEBrake = await initMainnetUser(cfg.eBrake, ethers.utils.parseEther("1")); + impersonatedComptroller = await initMainnetUser(cfg.comptroller, ethers.utils.parseEther("1")); + }); + + describe(`VIP-666 [${cfg.name}] — Pre-VIP behaviour`, () => { + it("DeviationSentinel pendingOwner is Normal Timelock", async () => { + expect(await deviationSentinel.pendingOwner()).to.equal(cfg.normalTimelock); + }); + + it("SentinelOracle pendingOwner is Normal Timelock", async () => { + expect(await sentinelOracle.pendingOwner()).to.equal(cfg.normalTimelock); + }); + + it("UniswapOracle pendingOwner is Normal Timelock", async () => { + expect(await uniswapOracle.pendingOwner()).to.equal(cfg.normalTimelock); + }); + + it("EBrake pendingOwner is Normal Timelock", async () => { + expect(await eBrake.pendingOwner()).to.equal(cfg.normalTimelock); + }); + + it("EBrake immutables are correctly set for IL", async () => { + expect(await eBrake.COMPTROLLER()).to.equal(cfg.comptroller); + expect(await eBrake.IS_ISOLATED_POOL()).to.equal(true); + }); + + it("DeviationSentinel immutables wire to local EBrake + SentinelOracle", async () => { + expect(await deviationSentinel.EBRAKE()).to.equal(cfg.eBrake); + expect(await deviationSentinel.SENTINEL_ORACLE()).to.equal(cfg.sentinelOracle); + }); + + it("Guardian + Timelocks have no admin permissions on DeviationSentinel yet", async () => { + const a = acm.connect(impersonatedDeviationSentinel); + for (const account of governanceAccounts) { + for (const sig of SENTINEL_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + + it("Guardian + Timelocks have no admin permissions on SentinelOracle yet", async () => { + const a = acm.connect(impersonatedSentinelOracle); + for (const account of governanceAccounts) { + for (const sig of SENTINEL_ORACLE_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + + it("Guardian + Timelocks have no admin permissions on UniswapOracle yet", async () => { + const a = acm.connect(impersonatedUniswapOracle); + for (const account of governanceAccounts) { + for (const sig of UNISWAP_ORACLE_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + + it("EBrake has no permissions on the IL Comptroller yet", async () => { + const a = acm.connect(impersonatedComptroller); + for (const sig of EBRAKE_COMPTROLLER_PERMS_IL) { + expect(await a.isAllowedToCall(cfg.eBrake, sig)).to.equal(false, `unexpected ${sig}`); + } + }); + + it("Guardian + Timelocks have no reset permissions on EBrake yet", async () => { + const a = acm.connect(impersonatedEBrake); + for (const account of governanceAccounts) { + for (const sig of RESET_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + + it("DeviationSentinel has no action permissions on EBrake yet", async () => { + const a = acm.connect(impersonatedEBrake); + for (const sig of SENTINEL_EBRAKE_PERMS) { + expect(await a.isAllowedToCall(cfg.deviationSentinel, sig)).to.equal(false, `unexpected ${sig}`); + } + }); + + it("Guardian + Timelocks have no EBrake action permissions yet", async () => { + const a = acm.connect(impersonatedEBrake); + for (const account of governanceAccounts) { + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + + it("Multisig Pauser has no EBrake action permissions yet", async () => { + const a = acm.connect(impersonatedEBrake); + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await a.isAllowedToCall(cfg.multisigPauser, sig)).to.equal(false, `unexpected ${sig}`); + } + }); + + it("No accounts are whitelisted as trusted keepers yet", async () => { + for (const account of trustedKeeperAccounts) { + expect(await deviationSentinel.trustedKeepers(account)).to.equal(false); + } + }); + + if (cfg.monitoredToken !== ZERO_ADDRESS) { + it("Monitored token has no pool configured on UniswapOracle yet", async () => { + expect(await uniswapOracle.tokenPools(cfg.monitoredToken)).to.equal(ZERO_ADDRESS); + }); + + it("Monitored token has no oracle configured on SentinelOracle yet", async () => { + const tc = await sentinelOracle.tokenConfigs(cfg.monitoredToken); + // tokenConfigs returns an address (the oracle address) packed as a struct; + // accessing .oracle works whether the ABI exposes it as a struct or just a single value. + expect(tc.oracle ?? tc).to.equal(ZERO_ADDRESS); + }); + + it("Monitored token has no deviation config on DeviationSentinel yet", async () => { + const tc = await deviationSentinel.tokenConfigs(cfg.monitoredToken); + expect(tc.deviation).to.equal(0); + expect(tc.enabled).to.equal(false); + }); + } + }); + + testForkedNetworkVipCommands(`VIP-666 [${cfg.name}] Configure DeviationSentinel + EBrakeV2`, await vip666(), { + callbackAfterExecution: async txResponse => { + // 4 acceptOwnership() calls per chain + await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["OwnershipTransferred"], [4]); + // 5 trusted keepers whitelisted per chain + await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TrustedKeeperUpdated"], [5]); + // 83 RoleGranted events per chain (token wiring not counted; gated by monitoredToken) + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [PERMS_GRANTED_PER_CHAIN]); + }, + }); + + describe(`VIP-666 [${cfg.name}] — Post-VIP behaviour`, () => { + it("All four contracts have Normal Timelock as owner", async () => { + expect(await deviationSentinel.owner()).to.equal(cfg.normalTimelock); + expect(await sentinelOracle.owner()).to.equal(cfg.normalTimelock); + expect(await uniswapOracle.owner()).to.equal(cfg.normalTimelock); + expect(await eBrake.owner()).to.equal(cfg.normalTimelock); + }); + + it("All four contracts have AddressZero as pendingOwner (acceptOwnership cleared it)", async () => { + expect(await deviationSentinel.pendingOwner()).to.equal(ZERO_ADDRESS); + expect(await sentinelOracle.pendingOwner()).to.equal(ZERO_ADDRESS); + expect(await uniswapOracle.pendingOwner()).to.equal(ZERO_ADDRESS); + expect(await eBrake.pendingOwner()).to.equal(ZERO_ADDRESS); + }); + + it("Guardian + Timelocks have all admin permissions on DeviationSentinel", async () => { + const a = acm.connect(impersonatedDeviationSentinel); + for (const account of governanceAccounts) { + for (const sig of SENTINEL_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + + it("Guardian + Timelocks have all admin permissions on SentinelOracle", async () => { + const a = acm.connect(impersonatedSentinelOracle); + for (const account of governanceAccounts) { + for (const sig of SENTINEL_ORACLE_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + + it("Guardian + Timelocks have setPoolConfig permission on UniswapOracle", async () => { + const a = acm.connect(impersonatedUniswapOracle); + for (const account of governanceAccounts) { + for (const sig of UNISWAP_ORACLE_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + + it("EBrake has all four IL-supported permissions on the local IL Comptroller", async () => { + const a = acm.connect(impersonatedComptroller); + for (const sig of EBRAKE_COMPTROLLER_PERMS_IL) { + expect(await a.isAllowedToCall(cfg.eBrake, sig)).to.equal(true, `missing ${sig}`); + } + }); + + it("Guardian + Timelocks have all reset permissions on EBrake", async () => { + const a = acm.connect(impersonatedEBrake); + for (const account of governanceAccounts) { + for (const sig of RESET_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + + it("DeviationSentinel has the three handleDeviation permissions on EBrake", async () => { + const a = acm.connect(impersonatedEBrake); + for (const sig of SENTINEL_EBRAKE_PERMS) { + expect(await a.isAllowedToCall(cfg.deviationSentinel, sig)).to.equal(true, `missing ${sig}`); + } + }); + + it("Guardian + Timelocks have all 8 IL-supported EBrake action permissions", async () => { + const a = acm.connect(impersonatedEBrake); + for (const account of governanceAccounts) { + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + + it("Multisig Pauser has all 8 IL-supported EBrake action permissions", async () => { + const a = acm.connect(impersonatedEBrake); + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await a.isAllowedToCall(cfg.multisigPauser, sig)).to.equal(true, `missing ${sig}`); + } + }); + + it("Diamond-only EBrake permissions are intentionally NOT granted", async () => { + const a = acm.connect(impersonatedEBrake); + for (const account of governanceAccounts) { + for (const sig of DIAMOND_ONLY_EBRAKE_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal( + false, + `Diamond-only ${sig} unexpectedly granted to ${account}`, + ); + } + } + }); + + it("Keeper + Guardian + Timelocks are whitelisted as trusted keepers on DeviationSentinel", async () => { + for (const account of trustedKeeperAccounts) { + expect(await deviationSentinel.trustedKeepers(account)).to.equal(true); + } + }); + + if (cfg.monitoredToken !== ZERO_ADDRESS) { + it("Monitored token's pool is configured on UniswapOracle", async () => { + expect(await uniswapOracle.tokenPools(cfg.monitoredToken)).to.equal(cfg.monitoredPool); + }); + + it("Monitored token's oracle is configured on SentinelOracle", async () => { + const tc = await sentinelOracle.tokenConfigs(cfg.monitoredToken); + expect(tc.oracle ?? tc).to.equal(cfg.uniswapOracle); + }); + + it("Monitored token's deviation threshold is configured on DeviationSentinel", async () => { + const tc = await deviationSentinel.tokenConfigs(cfg.monitoredToken); + expect(tc.deviation).to.equal(cfg.deviationPercent); + expect(tc.enabled).to.equal(true); + }); + } + }); + + describe(`VIP-666 [${cfg.name}] — Functional sanity checks`, () => { + let randomEoa: SignerWithAddress; + let normalTimelockSigner: SignerWithAddress; + + before(async () => { + [randomEoa] = await ethers.getSigners(); + normalTimelockSigner = await initMainnetUser(cfg.normalTimelock, ethers.utils.parseEther("10")); + }); + + it("Random EOA cannot call DeviationSentinel.setTrustedKeeper (no ACM perm)", async () => { + await expect(deviationSentinel.connect(randomEoa).setTrustedKeeper(randomEoa.address, true)).to.be.reverted; + }); + + it("Normal Timelock can call DeviationSentinel.setTrustedKeeper (perm granted by VIP)", async () => { + const probe = ethers.Wallet.createRandom().address; + await expect(deviationSentinel.connect(normalTimelockSigner).setTrustedKeeper(probe, true)) + .to.emit(deviationSentinel, "TrustedKeeperUpdated") + .withArgs(probe, true); + expect(await deviationSentinel.trustedKeepers(probe)).to.equal(true); + }); + + it("Random EOA cannot call EBrake.pauseBorrow (no ACM perm)", async () => { + const market = ethers.Wallet.createRandom().address; + await expect(eBrake.connect(randomEoa).pauseBorrow(market)).to.be.reverted; + }); + + it("DeviationSentinel.handleDeviation reverts UnauthorizedKeeper for a non-keeper EOA", async () => { + const market = ethers.Wallet.createRandom().address; + await expect(deviationSentinel.connect(randomEoa).handleDeviation(market)).to.be.reverted; + }); + }); +}; diff --git a/simulations/vip-666/zksyncmainnet.ts b/simulations/vip-666/zksyncmainnet.ts new file mode 100644 index 000000000..970551fc4 --- /dev/null +++ b/simulations/vip-666/zksyncmainnet.ts @@ -0,0 +1,11 @@ +import { forking } from "src/vip-framework"; + +import { ZKSYNCMAINNET_CONFIG } from "../../vips/vip-666/addresses/zksyncmainnet"; +import { runVip666Suite } from "./shared"; + +// TODO: set fork block once the EBrake/DeviationSentinel stack is deployed on zkSync Era. +const FORK_BLOCK = 0; + +forking(FORK_BLOCK, async () => { + await runVip666Suite(ZKSYNCMAINNET_CONFIG); +}); diff --git a/src/networkAddresses.ts b/src/networkAddresses.ts index 21cb44e94..68e8685e9 100644 --- a/src/networkAddresses.ts +++ b/src/networkAddresses.ts @@ -167,6 +167,7 @@ export const NETWORK_ADDRESSES = { OMNICHAIN_GOVERNANCE_EXECUTOR: "0xcf3e6972a8e9c53D33b642a2592938944956f138", }, arbitrumone: { + ACCESS_CONTROL_MANAGER: "0xD9dD18EB0cf10CbA837677f28A8F9Bda4bc2b157", NORMAL_TIMELOCK: "0x4b94589Cc23F618687790036726f744D602c4017", FAST_TRACK_TIMELOCK: "0x2286a9B2a5246218f2fC1F380383f45BDfCE3E04", CRITICAL_TIMELOCK: "0x181E4f8F21D087bF02Ea2F64D5e550849FBca674", @@ -202,6 +203,7 @@ export const NETWORK_ADDRESSES = { OMNICHAIN_GOVERNANCE_EXECUTOR: "0x83F79CfbaEee738173c0dfd866796743F4E25C9e", }, zksyncmainnet: { + ACCESS_CONTROL_MANAGER: "0x526159A92A82afE5327d37Ef446b68FD9a5cA914", NORMAL_TIMELOCK: "0x093565Bc20AA326F4209eBaF3a26089272627613", FAST_TRACK_TIMELOCK: "0x32f71c95BC8F9d996f89c642f1a84d06B2484AE9", CRITICAL_TIMELOCK: "0xbfbc79D4198963e4a66270F3EfB1fdA0F382E49c", @@ -271,6 +273,7 @@ export const NETWORK_ADDRESSES = { ENDPOINT: "0x55370E0fBB5f5b8dAeD978BA1c075a499eB107B8", }, basemainnet: { + ACCESS_CONTROL_MANAGER: "0x9E6CeEfDC6183e4D0DF8092A9B90cDF659687daB", NORMAL_TIMELOCK: "0x21c12f2946a1a66cBFf7eb997022a37167eCf517", FAST_TRACK_TIMELOCK: "0x209F73Ee2Fa9A72aF3Fa6aF1933A3B58ed3De5D7", CRITICAL_TIMELOCK: "0x47F65466392ff2aE825d7a170889F7b5b9D8e60D", diff --git a/vips/vip-666/addresses/arbitrumone.ts b/vips/vip-666/addresses/arbitrumone.ts new file mode 100644 index 000000000..781e72689 --- /dev/null +++ b/vips/vip-666/addresses/arbitrumone.ts @@ -0,0 +1,47 @@ +import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { LzChainId } from "src/types"; + +const { arbitrumone } = NETWORK_ADDRESSES; + +// Already-deployed governance + protocol addresses on Arbitrum One +export const ARBITRUMONE_ACM = arbitrumone.ACCESS_CONTROL_MANAGER; +export const ARBITRUMONE_GUARDIAN = arbitrumone.GUARDIAN; +export const ARBITRUMONE_NORMAL_TIMELOCK = arbitrumone.NORMAL_TIMELOCK; +export const ARBITRUMONE_FAST_TRACK_TIMELOCK = arbitrumone.FAST_TRACK_TIMELOCK; +export const ARBITRUMONE_CRITICAL_TIMELOCK = arbitrumone.CRITICAL_TIMELOCK; +export const ARBITRUMONE_CORE_COMPTROLLER = arbitrumone.CORE_COMPTROLLER; + +// PLACEHOLDERS — to be filled once the EBrake/DeviationSentinel stack is deployed on Arbitrum One +export const ARBITRUMONE_DEVIATION_SENTINEL = ZERO_ADDRESS; +export const ARBITRUMONE_EBRAKE = ZERO_ADDRESS; +export const ARBITRUMONE_SENTINEL_ORACLE = ZERO_ADDRESS; +export const ARBITRUMONE_UNISWAP_ORACLE = ZERO_ADDRESS; +export const ARBITRUMONE_MULTISIG_PAUSER = ZERO_ADDRESS; +export const ARBITRUMONE_KEEPER = ZERO_ADDRESS; + +// PLACEHOLDER — first monitored token + its Uniswap V3 pool. Leave as ZERO_ADDRESS to skip token wiring. +export const ARBITRUMONE_MONITORED_TOKEN = ZERO_ADDRESS; +export const ARBITRUMONE_MONITORED_POOL = ZERO_ADDRESS; +export const ARBITRUMONE_DEVIATION_PERCENT = 20; + +export const ARBITRUMONE_DST_CHAIN_ID = LzChainId.arbitrumone; + +export const ARBITRUMONE_CONFIG = { + name: "Arbitrum One", + dstChainId: ARBITRUMONE_DST_CHAIN_ID, + acm: ARBITRUMONE_ACM, + guardian: ARBITRUMONE_GUARDIAN, + normalTimelock: ARBITRUMONE_NORMAL_TIMELOCK, + fastTrackTimelock: ARBITRUMONE_FAST_TRACK_TIMELOCK, + criticalTimelock: ARBITRUMONE_CRITICAL_TIMELOCK, + comptroller: ARBITRUMONE_CORE_COMPTROLLER, + deviationSentinel: ARBITRUMONE_DEVIATION_SENTINEL, + eBrake: ARBITRUMONE_EBRAKE, + sentinelOracle: ARBITRUMONE_SENTINEL_ORACLE, + uniswapOracle: ARBITRUMONE_UNISWAP_ORACLE, + multisigPauser: ARBITRUMONE_MULTISIG_PAUSER, + keeper: ARBITRUMONE_KEEPER, + monitoredToken: ARBITRUMONE_MONITORED_TOKEN, + monitoredPool: ARBITRUMONE_MONITORED_POOL, + deviationPercent: ARBITRUMONE_DEVIATION_PERCENT, +} as const; diff --git a/vips/vip-666/addresses/basemainnet.ts b/vips/vip-666/addresses/basemainnet.ts new file mode 100644 index 000000000..07a10280c --- /dev/null +++ b/vips/vip-666/addresses/basemainnet.ts @@ -0,0 +1,47 @@ +import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { LzChainId } from "src/types"; + +const { basemainnet } = NETWORK_ADDRESSES; + +// Already-deployed governance + protocol addresses on Base mainnet +export const BASEMAINNET_ACM = basemainnet.ACCESS_CONTROL_MANAGER; +export const BASEMAINNET_GUARDIAN = basemainnet.GUARDIAN; +export const BASEMAINNET_NORMAL_TIMELOCK = basemainnet.NORMAL_TIMELOCK; +export const BASEMAINNET_FAST_TRACK_TIMELOCK = basemainnet.FAST_TRACK_TIMELOCK; +export const BASEMAINNET_CRITICAL_TIMELOCK = basemainnet.CRITICAL_TIMELOCK; +export const BASEMAINNET_CORE_COMPTROLLER = basemainnet.CORE_COMPTROLLER; + +// PLACEHOLDERS — to be filled once the EBrake/DeviationSentinel stack is deployed on Base +export const BASEMAINNET_DEVIATION_SENTINEL = ZERO_ADDRESS; +export const BASEMAINNET_EBRAKE = ZERO_ADDRESS; +export const BASEMAINNET_SENTINEL_ORACLE = ZERO_ADDRESS; +export const BASEMAINNET_UNISWAP_ORACLE = ZERO_ADDRESS; +export const BASEMAINNET_MULTISIG_PAUSER = ZERO_ADDRESS; +export const BASEMAINNET_KEEPER = ZERO_ADDRESS; + +// PLACEHOLDER — first monitored token + its Uniswap V3 pool. Leave as ZERO_ADDRESS to skip token wiring. +export const BASEMAINNET_MONITORED_TOKEN = ZERO_ADDRESS; +export const BASEMAINNET_MONITORED_POOL = ZERO_ADDRESS; +export const BASEMAINNET_DEVIATION_PERCENT = 20; + +export const BASEMAINNET_DST_CHAIN_ID = LzChainId.basemainnet; + +export const BASEMAINNET_CONFIG = { + name: "Base", + dstChainId: BASEMAINNET_DST_CHAIN_ID, + acm: BASEMAINNET_ACM, + guardian: BASEMAINNET_GUARDIAN, + normalTimelock: BASEMAINNET_NORMAL_TIMELOCK, + fastTrackTimelock: BASEMAINNET_FAST_TRACK_TIMELOCK, + criticalTimelock: BASEMAINNET_CRITICAL_TIMELOCK, + comptroller: BASEMAINNET_CORE_COMPTROLLER, + deviationSentinel: BASEMAINNET_DEVIATION_SENTINEL, + eBrake: BASEMAINNET_EBRAKE, + sentinelOracle: BASEMAINNET_SENTINEL_ORACLE, + uniswapOracle: BASEMAINNET_UNISWAP_ORACLE, + multisigPauser: BASEMAINNET_MULTISIG_PAUSER, + keeper: BASEMAINNET_KEEPER, + monitoredToken: BASEMAINNET_MONITORED_TOKEN, + monitoredPool: BASEMAINNET_MONITORED_POOL, + deviationPercent: BASEMAINNET_DEVIATION_PERCENT, +} as const; diff --git a/vips/vip-666/addresses/ethereum.ts b/vips/vip-666/addresses/ethereum.ts new file mode 100644 index 000000000..92e2e1283 --- /dev/null +++ b/vips/vip-666/addresses/ethereum.ts @@ -0,0 +1,47 @@ +import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { LzChainId } from "src/types"; + +const { ethereum } = NETWORK_ADDRESSES; + +// Already-deployed governance + protocol addresses on Ethereum mainnet +export const ETHEREUM_ACM = ethereum.ACCESS_CONTROL_MANAGER; +export const ETHEREUM_GUARDIAN = ethereum.GUARDIAN; +export const ETHEREUM_NORMAL_TIMELOCK = ethereum.NORMAL_TIMELOCK; +export const ETHEREUM_FAST_TRACK_TIMELOCK = ethereum.FAST_TRACK_TIMELOCK; +export const ETHEREUM_CRITICAL_TIMELOCK = ethereum.CRITICAL_TIMELOCK; +export const ETHEREUM_CORE_COMPTROLLER = ethereum.CORE_COMPTROLLER; + +// PLACEHOLDERS — to be filled once the EBrake/DeviationSentinel stack is deployed on Ethereum +export const ETHEREUM_DEVIATION_SENTINEL = ZERO_ADDRESS; +export const ETHEREUM_EBRAKE = ZERO_ADDRESS; +export const ETHEREUM_SENTINEL_ORACLE = ZERO_ADDRESS; +export const ETHEREUM_UNISWAP_ORACLE = ZERO_ADDRESS; +export const ETHEREUM_MULTISIG_PAUSER = ZERO_ADDRESS; +export const ETHEREUM_KEEPER = ZERO_ADDRESS; + +// PLACEHOLDER — first monitored token + its Uniswap V3 pool. Leave as ZERO_ADDRESS to skip token wiring. +export const ETHEREUM_MONITORED_TOKEN = ZERO_ADDRESS; +export const ETHEREUM_MONITORED_POOL = ZERO_ADDRESS; +export const ETHEREUM_DEVIATION_PERCENT = 20; + +export const ETHEREUM_DST_CHAIN_ID = LzChainId.ethereum; + +export const ETHEREUM_CONFIG = { + name: "Ethereum", + dstChainId: ETHEREUM_DST_CHAIN_ID, + acm: ETHEREUM_ACM, + guardian: ETHEREUM_GUARDIAN, + normalTimelock: ETHEREUM_NORMAL_TIMELOCK, + fastTrackTimelock: ETHEREUM_FAST_TRACK_TIMELOCK, + criticalTimelock: ETHEREUM_CRITICAL_TIMELOCK, + comptroller: ETHEREUM_CORE_COMPTROLLER, + deviationSentinel: ETHEREUM_DEVIATION_SENTINEL, + eBrake: ETHEREUM_EBRAKE, + sentinelOracle: ETHEREUM_SENTINEL_ORACLE, + uniswapOracle: ETHEREUM_UNISWAP_ORACLE, + multisigPauser: ETHEREUM_MULTISIG_PAUSER, + keeper: ETHEREUM_KEEPER, + monitoredToken: ETHEREUM_MONITORED_TOKEN, + monitoredPool: ETHEREUM_MONITORED_POOL, + deviationPercent: ETHEREUM_DEVIATION_PERCENT, +} as const; diff --git a/vips/vip-666/addresses/zksyncmainnet.ts b/vips/vip-666/addresses/zksyncmainnet.ts new file mode 100644 index 000000000..ead929164 --- /dev/null +++ b/vips/vip-666/addresses/zksyncmainnet.ts @@ -0,0 +1,47 @@ +import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { LzChainId } from "src/types"; + +const { zksyncmainnet } = NETWORK_ADDRESSES; + +// Already-deployed governance + protocol addresses on zkSync Era mainnet +export const ZKSYNCMAINNET_ACM = zksyncmainnet.ACCESS_CONTROL_MANAGER; +export const ZKSYNCMAINNET_GUARDIAN = zksyncmainnet.GUARDIAN; +export const ZKSYNCMAINNET_NORMAL_TIMELOCK = zksyncmainnet.NORMAL_TIMELOCK; +export const ZKSYNCMAINNET_FAST_TRACK_TIMELOCK = zksyncmainnet.FAST_TRACK_TIMELOCK; +export const ZKSYNCMAINNET_CRITICAL_TIMELOCK = zksyncmainnet.CRITICAL_TIMELOCK; +export const ZKSYNCMAINNET_CORE_COMPTROLLER = zksyncmainnet.CORE_COMPTROLLER; + +// PLACEHOLDERS — to be filled once the EBrake/DeviationSentinel stack is deployed on zkSync Era +export const ZKSYNCMAINNET_DEVIATION_SENTINEL = ZERO_ADDRESS; +export const ZKSYNCMAINNET_EBRAKE = ZERO_ADDRESS; +export const ZKSYNCMAINNET_SENTINEL_ORACLE = ZERO_ADDRESS; +export const ZKSYNCMAINNET_UNISWAP_ORACLE = ZERO_ADDRESS; +export const ZKSYNCMAINNET_MULTISIG_PAUSER = ZERO_ADDRESS; +export const ZKSYNCMAINNET_KEEPER = ZERO_ADDRESS; + +// PLACEHOLDER — first monitored token + its DEX pool. Leave as ZERO_ADDRESS to skip token wiring. +export const ZKSYNCMAINNET_MONITORED_TOKEN = ZERO_ADDRESS; +export const ZKSYNCMAINNET_MONITORED_POOL = ZERO_ADDRESS; +export const ZKSYNCMAINNET_DEVIATION_PERCENT = 20; + +export const ZKSYNCMAINNET_DST_CHAIN_ID = LzChainId.zksyncmainnet; + +export const ZKSYNCMAINNET_CONFIG = { + name: "zkSync Era", + dstChainId: ZKSYNCMAINNET_DST_CHAIN_ID, + acm: ZKSYNCMAINNET_ACM, + guardian: ZKSYNCMAINNET_GUARDIAN, + normalTimelock: ZKSYNCMAINNET_NORMAL_TIMELOCK, + fastTrackTimelock: ZKSYNCMAINNET_FAST_TRACK_TIMELOCK, + criticalTimelock: ZKSYNCMAINNET_CRITICAL_TIMELOCK, + comptroller: ZKSYNCMAINNET_CORE_COMPTROLLER, + deviationSentinel: ZKSYNCMAINNET_DEVIATION_SENTINEL, + eBrake: ZKSYNCMAINNET_EBRAKE, + sentinelOracle: ZKSYNCMAINNET_SENTINEL_ORACLE, + uniswapOracle: ZKSYNCMAINNET_UNISWAP_ORACLE, + multisigPauser: ZKSYNCMAINNET_MULTISIG_PAUSER, + keeper: ZKSYNCMAINNET_KEEPER, + monitoredToken: ZKSYNCMAINNET_MONITORED_TOKEN, + monitoredPool: ZKSYNCMAINNET_MONITORED_POOL, + deviationPercent: ZKSYNCMAINNET_DEVIATION_PERCENT, +} as const; diff --git a/vips/vip-666/bscmainnet.ts b/vips/vip-666/bscmainnet.ts new file mode 100644 index 000000000..142d2c6c2 --- /dev/null +++ b/vips/vip-666/bscmainnet.ts @@ -0,0 +1,225 @@ +import { ZERO_ADDRESS } from "src/networkAddresses"; +import { Command, ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { ARBITRUMONE_CONFIG } from "./addresses/arbitrumone"; +import { BASEMAINNET_CONFIG } from "./addresses/basemainnet"; +import { ETHEREUM_CONFIG } from "./addresses/ethereum"; +import { ZKSYNCMAINNET_CONFIG } from "./addresses/zksyncmainnet"; + +// Per-chain configuration shape. Each chain bundles its ACM, governance accounts, +// IL Comptroller, and the EBrake/DeviationSentinel stack addresses (placeholders +// until contracts are deployed on the remote chain). +export interface ChainConfig { + name: string; + dstChainId: number; + acm: string; + guardian: string; + normalTimelock: string; + fastTrackTimelock: string; + criticalTimelock: string; + comptroller: string; + deviationSentinel: string; + eBrake: string; + sentinelOracle: string; + uniswapOracle: string; + multisigPauser: string; + keeper: string; + monitoredToken: string; + monitoredPool: string; + deviationPercent: number; +} + +const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, ZKSYNCMAINNET_CONFIG, BASEMAINNET_CONFIG]; + +// DeviationSentinel access-controlled functions (3 total — setTokenConfig uses +// the struct-tuple form (uint8,bool) verbatim, matching DeviationSentinel.sol). +export const SENTINEL_ADMIN_PERMS = [ + "setTrustedKeeper(address,bool)", + "setTokenConfig(address,(uint8,bool))", + "setTokenMonitoringEnabled(address,bool)", +]; + +// SentinelOracle access-controlled functions +export const SENTINEL_ORACLE_ADMIN_PERMS = ["setTokenOracleConfig(address,address)", "setDirectPrice(address,uint256)"]; + +// UniswapOracle access-controlled functions +export const UNISWAP_ORACLE_ADMIN_PERMS = ["setPoolConfig(address,address)"]; + +// IL Comptroller permissions for EBrake. +// setActionsPaused uses uint256[] (not uint8[]) in the IL Comptroller. +export const EBRAKE_COMPTROLLER_PERMS_IL = [ + "setActionsPaused(address[],uint256[],bool)", + "setCollateralFactor(address,uint256,uint256)", + "setMarketBorrowCaps(address[],uint256[])", + "setMarketSupplyCaps(address[],uint256[])", +]; + +// Granular snapshot-reset functions governance Timelocks and Guardian can call on EBrake. +export const RESET_PERMS = [ + "resetCFSnapshot(address)", + "resetBorrowCapSnapshot(address)", + "resetSupplyCapSnapshot(address)", +]; + +// EBrake functions DeviationSentinel.handleDeviation invokes (single-arg decreaseCF +// is the IL-supported form). +export const SENTINEL_EBRAKE_PERMS = ["pauseBorrow(address)", "pauseSupply(address)", "decreaseCF(address,uint256)"]; + +// IL-supported subset of EBrake action functions granted to governance + multisig. +// Excludes: pauseFlashLoan(), disablePoolBorrow(uint96,address), +// revokeFlashLoanAccess(address), decreaseCF(address,uint96,uint256) — all revert +// on isIsolatedPool=true (they target Diamond-only Comptroller methods). +export const GOVERNANCE_EBRAKE_PERMS_IL = [ + "pauseSupply(address)", + "pauseRedeem(address)", + "pauseBorrow(address)", + "pauseTransfer(address)", + "pauseActions(address[],uint8[])", + "setMarketBorrowCaps(address[],uint256[])", + "setMarketSupplyCaps(address[],uint256[])", + "decreaseCF(address,uint256)", +]; + +// Diamond-only EBrake fns deliberately NOT granted on IL chains (would revert). +export const DIAMOND_ONLY_EBRAKE_PERMS = [ + "pauseFlashLoan()", + "disablePoolBorrow(uint96,address)", + "revokeFlashLoanAccess(address)", + "decreaseCF(address,uint96,uint256)", +]; + +const grant = (acm: string, contract: string, sig: string, account: string, dstChainId: number) => ({ + target: acm, + signature: "giveCallPermission(address,string,address)", + params: [contract, sig, account], + dstChainId, +}); + +const buildChainCommands = (cfg: ChainConfig) => { + const { acm, dstChainId } = cfg; + const governanceAccounts = [cfg.guardian, cfg.normalTimelock, cfg.fastTrackTimelock, cfg.criticalTimelock]; + const trustedKeeperAccounts = [cfg.keeper, ...governanceAccounts]; + + const commands: Command[] = [ + // 1. Accept ownership of the four newly deployed contracts. The deployer + // transfers ownership to the local Normal Timelock prior to this VIP. + ...[cfg.deviationSentinel, cfg.sentinelOracle, cfg.uniswapOracle, cfg.eBrake].map(target => ({ + target, + signature: "acceptOwnership()", + params: [], + dstChainId, + })), + + // 2. Grant Guardian + governance Timelocks admin permissions on DeviationSentinel + ...governanceAccounts.flatMap(account => + SENTINEL_ADMIN_PERMS.map(sig => grant(acm, cfg.deviationSentinel, sig, account, dstChainId)), + ), + + // 3. Grant Guardian + governance Timelocks admin permissions on SentinelOracle + ...governanceAccounts.flatMap(account => + SENTINEL_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.sentinelOracle, sig, account, dstChainId)), + ), + + // 4. Grant Guardian + governance Timelocks admin permission on UniswapOracle + ...governanceAccounts.flatMap(account => + UNISWAP_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.uniswapOracle, sig, account, dstChainId)), + ), + + // 5. Grant EBrake the IL-Comptroller-supported emergency-action permissions + ...EBRAKE_COMPTROLLER_PERMS_IL.map(sig => grant(acm, cfg.comptroller, sig, cfg.eBrake, dstChainId)), + + // 6. Grant Guardian + governance Timelocks granular snapshot-reset perms on EBrake + ...governanceAccounts.flatMap(account => RESET_PERMS.map(sig => grant(acm, cfg.eBrake, sig, account, dstChainId))), + + // 7. Grant DeviationSentinel the three EBrake actions handleDeviation invokes + ...SENTINEL_EBRAKE_PERMS.map(sig => grant(acm, cfg.eBrake, sig, cfg.deviationSentinel, dstChainId)), + + // 8. Grant Guardian + governance Timelocks the IL-supported EBrake action functions + ...governanceAccounts.flatMap(account => + GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, account, dstChainId)), + ), + + // 9. Whitelist Keeper + Guardian + governance Timelocks as trusted keepers on + // DeviationSentinel so VIPs (and the off-chain keeper) can invoke handleDeviation. + ...trustedKeeperAccounts.map(account => ({ + target: cfg.deviationSentinel, + signature: "setTrustedKeeper(address,bool)", + params: [account, true], + dstChainId, + })), + + // 10. Grant the per-chain 1-of-1 Multisig Pauser the same IL-supported EBrake + // action functions, so the Venus team can manually trigger emergency + // actions during the early operational phase (mirror of VIP-610 step 7). + ...GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, cfg.multisigPauser, dstChainId)), + ]; + + // 11. Token-specific wiring. Skipped when no token is yet chosen for the chain + // (monitoredToken == ZERO_ADDRESS) so the proposal stays executable + // even with placeholder addresses. + if (cfg.monitoredToken !== ZERO_ADDRESS) { + commands.push( + { + target: cfg.uniswapOracle, + signature: "setPoolConfig(address,address)", + params: [cfg.monitoredToken, cfg.monitoredPool], + dstChainId, + }, + { + target: cfg.sentinelOracle, + signature: "setTokenOracleConfig(address,address)", + params: [cfg.monitoredToken, cfg.uniswapOracle], + dstChainId, + }, + { + target: cfg.deviationSentinel, + signature: "setTokenConfig(address,(uint8,bool))", + params: [cfg.monitoredToken, [cfg.deviationPercent, true]], + dstChainId, + }, + ); + } + + return commands; +}; + +export const vip666 = () => { + const meta = { + version: "v2", + title: "VIP-666 [Ethereum, Arbitrum One, zkSync Era, Base] Configure DeviationSentinel + EBrakeV2", + description: `#### Description + +This VIP configures the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, **zkSync Era**, and **Base**, mirroring the BSC setup from VIP-590 + VIP-610. Each chain's DeviationSentinel routes automated oracle-deviation enforcement through a local EBrakeV2, which applies per-action, per-market restrictions (pause borrow/supply, zero collateral factor) without manual intervention. + +Because EBrake on these chains uses \`isIsolatedPool=true\` (single-pool IL Comptroller, not the BSC Diamond), only the IL-supported subset of EBrake action functions is granted. Diamond-only functions (\`pauseFlashLoan\`, \`disablePoolBorrow\`, \`revokeFlashLoanAccess\`, \`decreaseCF(address,uint96,uint256)\`) are omitted as they revert on IL comptrollers. + +#### Summary + +If approved, this VIP will, for each of Ethereum, Arbitrum One, zkSync Era, and Base: + +- Accept governance ownership of the **DeviationSentinel**, **SentinelOracle**, **UniswapOracle**, and **EBrakeV2** contracts +- Grant admin permissions on DeviationSentinel, SentinelOracle, and UniswapOracle to Guardian + 3 Timelocks +- Grant **EBrakeV2** the 4 IL-supported Comptroller permissions it needs to execute emergency actions +- Authorize **DeviationSentinel** to call \`pauseBorrow\`, \`pauseSupply\`, and \`decreaseCF\` on EBrake +- Grant **Guardian** and governance **Timelocks** the 8 IL-supported EBrake action functions and granular snapshot-reset permissions +- Grant the **per-chain 1-of-1 Multisig Pauser** the 8 IL-supported EBrake action functions for manual emergency pausing (Phase 0) +- Whitelist Keeper + Guardian + 3 Timelocks as trusted keepers on DeviationSentinel + +**Permission event summary**: 332 PermissionGranted (83 per chain × 4 chains), 0 PermissionRevoked + +#### References + +- [VIP-590 (BSC)](https://app.venus.io/governance/proposal/590) +- [VIP-610 (BSC)](https://app.venus.io/governance/proposal/610) +- [Original Proposal: Emergency Brake — Price Deviation Safeguard Mechanism](https://community.venus.io/t/proposal-emergency-brake-price-deviation-safeguard-mechanism/5668) +- [GitHub PR](https://github.com/VenusProtocol/vips/pull/TODO)`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal(NETWORKS.flatMap(buildChainCommands), meta, ProposalType.REGULAR); +}; + +export default vip666; From d4f11124a9a750d9b5cbc0efd12cb9ce434cd7bf Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 28 Apr 2026 12:15:12 +0530 Subject: [PATCH 02/15] feat: expand monitored markets for Ethereum, Arbitrum One, Base, and zkSync in VIP-666 --- simulations/vip-666/shared.ts | 45 ++++---- vips/vip-666/addresses/arbitrumone.ts | 46 +++++++-- vips/vip-666/addresses/basemainnet.ts | 39 +++++-- vips/vip-666/addresses/ethereum.ts | 76 ++++++++++++-- vips/vip-666/addresses/zksyncmainnet.ts | 21 ++-- vips/vip-666/bscmainnet.ts | 29 ++++-- vips/vip-666/market.md | 132 ++++++++++++++++++++++++ 7 files changed, 327 insertions(+), 61 deletions(-) create mode 100644 vips/vip-666/market.md diff --git a/simulations/vip-666/shared.ts b/simulations/vip-666/shared.ts index 759233714..0b57acf47 100644 --- a/simulations/vip-666/shared.ts +++ b/simulations/vip-666/shared.ts @@ -23,7 +23,8 @@ import EBRAKE_ABI from "./abi/EBrake.json"; import SENTINEL_ORACLE_ABI from "./abi/SentinelOracle.json"; import UNISWAP_ORACLE_ABI from "./abi/UniswapOracle.json"; -// Total RoleGranted events per chain when no token wiring runs: +// Total RoleGranted events per chain (token wiring uses direct setter calls, +// not ACM grants, so it doesn't affect this count): // 3 (sentinel admin) × 4 + 2 (sentinel oracle admin) × 4 + 1 (uniswap oracle admin) × 4 // + 4 (ebrake → comptroller) + 3 (reset) × 4 + 3 (sentinel → ebrake) // + 8 (governance ebrake action) × 4 + 8 (multisig ebrake action) @@ -184,34 +185,39 @@ export const runVip666Suite = async (cfg: ChainConfig) => { } }); - if (cfg.monitoredToken !== ZERO_ADDRESS) { - it("Monitored token has no pool configured on UniswapOracle yet", async () => { - expect(await uniswapOracle.tokenPools(cfg.monitoredToken)).to.equal(ZERO_ADDRESS); + for (const market of cfg.monitoredMarkets) { + if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; + it(`${market.symbol} has no pool configured on UniswapOracle yet`, async () => { + expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); }); - it("Monitored token has no oracle configured on SentinelOracle yet", async () => { - const tc = await sentinelOracle.tokenConfigs(cfg.monitoredToken); - // tokenConfigs returns an address (the oracle address) packed as a struct; - // accessing .oracle works whether the ABI exposes it as a struct or just a single value. + it(`${market.symbol} has no oracle configured on SentinelOracle yet`, async () => { + const tc = await sentinelOracle.tokenConfigs(market.token); expect(tc.oracle ?? tc).to.equal(ZERO_ADDRESS); }); - it("Monitored token has no deviation config on DeviationSentinel yet", async () => { - const tc = await deviationSentinel.tokenConfigs(cfg.monitoredToken); + it(`${market.symbol} has no deviation config on DeviationSentinel yet`, async () => { + const tc = await deviationSentinel.tokenConfigs(market.token); expect(tc.deviation).to.equal(0); expect(tc.enabled).to.equal(false); }); } }); + const effectiveMarkets = cfg.monitoredMarkets.filter(m => m.token !== ZERO_ADDRESS && m.pool !== ZERO_ADDRESS); + testForkedNetworkVipCommands(`VIP-666 [${cfg.name}] Configure DeviationSentinel + EBrakeV2`, await vip666(), { callbackAfterExecution: async txResponse => { // 4 acceptOwnership() calls per chain await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["OwnershipTransferred"], [4]); // 5 trusted keepers whitelisted per chain await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TrustedKeeperUpdated"], [5]); - // 83 RoleGranted events per chain (token wiring not counted; gated by monitoredToken) + // 83 RoleGranted events per chain (token wiring uses direct setter calls, not ACM grants) await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [PERMS_GRANTED_PER_CHAIN]); + // 1 wiring event per market on each of UniswapOracle, SentinelOracle, DeviationSentinel + await expectEvents(txResponse, [UNISWAP_ORACLE_ABI], ["PoolConfigUpdated"], [effectiveMarkets.length]); + await expectEvents(txResponse, [SENTINEL_ORACLE_ABI], ["TokenOracleConfigUpdated"], [effectiveMarkets.length]); + await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TokenConfigUpdated"], [effectiveMarkets.length]); }, }); @@ -314,19 +320,20 @@ export const runVip666Suite = async (cfg: ChainConfig) => { } }); - if (cfg.monitoredToken !== ZERO_ADDRESS) { - it("Monitored token's pool is configured on UniswapOracle", async () => { - expect(await uniswapOracle.tokenPools(cfg.monitoredToken)).to.equal(cfg.monitoredPool); + for (const market of cfg.monitoredMarkets) { + if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; + it(`${market.symbol} pool is configured on UniswapOracle`, async () => { + expect(await uniswapOracle.tokenPools(market.token)).to.equal(market.pool); }); - it("Monitored token's oracle is configured on SentinelOracle", async () => { - const tc = await sentinelOracle.tokenConfigs(cfg.monitoredToken); + it(`${market.symbol} oracle is configured on SentinelOracle`, async () => { + const tc = await sentinelOracle.tokenConfigs(market.token); expect(tc.oracle ?? tc).to.equal(cfg.uniswapOracle); }); - it("Monitored token's deviation threshold is configured on DeviationSentinel", async () => { - const tc = await deviationSentinel.tokenConfigs(cfg.monitoredToken); - expect(tc.deviation).to.equal(cfg.deviationPercent); + it(`${market.symbol} deviation threshold is configured on DeviationSentinel`, async () => { + const tc = await deviationSentinel.tokenConfigs(market.token); + expect(tc.deviation).to.equal(market.deviationPercent); expect(tc.enabled).to.equal(true); }); } diff --git a/vips/vip-666/addresses/arbitrumone.ts b/vips/vip-666/addresses/arbitrumone.ts index 781e72689..a2faa49af 100644 --- a/vips/vip-666/addresses/arbitrumone.ts +++ b/vips/vip-666/addresses/arbitrumone.ts @@ -19,13 +19,45 @@ export const ARBITRUMONE_UNISWAP_ORACLE = ZERO_ADDRESS; export const ARBITRUMONE_MULTISIG_PAUSER = ZERO_ADDRESS; export const ARBITRUMONE_KEEPER = ZERO_ADDRESS; -// PLACEHOLDER — first monitored token + its Uniswap V3 pool. Leave as ZERO_ADDRESS to skip token wiring. -export const ARBITRUMONE_MONITORED_TOKEN = ZERO_ADDRESS; -export const ARBITRUMONE_MONITORED_POOL = ZERO_ADDRESS; -export const ARBITRUMONE_DEVIATION_PERCENT = 20; - export const ARBITRUMONE_DST_CHAIN_ID = LzChainId.arbitrumone; +// Eligible Core Pool markets — Uniswap V3 (Arbitrum) sources, unified 10% threshold. +// USD₮0 (Tether's bridged T-zero USDT) and the legacy USDT pool share the same +// USDC/USDT pool address per market spec. USD₮0 token address is left as a TODO +// placeholder — confirm and fill before proposing. +export const ARBITRUMONE_MONITORED_MARKETS = [ + { + symbol: "WETH", + token: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + pool: "0xc6962004f452be9203591991d15f6b388e09e8d0", // WETH/USDC Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "WBTC", + token: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", + pool: "0x0e4831319a50228b9e450861297ab92dee15b44f", // WBTC/USDC Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "USDC", + token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + pool: "0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6", // USDC/USDT Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "USD₮0", + token: ZERO_ADDRESS, // TODO: confirm USD₮0 token address on Arbitrum One + pool: "0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6", // USD₮0/USDC Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "ARB", + token: "0x912CE59144191C1204E64559FE8253a0e49E6548", + pool: "0xaebdca1bc8d89177ebe2308d62af5e74885dccc3", // ARB/USDC Uniswap V3 + deviationPercent: 10, + }, +]; + export const ARBITRUMONE_CONFIG = { name: "Arbitrum One", dstChainId: ARBITRUMONE_DST_CHAIN_ID, @@ -41,7 +73,5 @@ export const ARBITRUMONE_CONFIG = { uniswapOracle: ARBITRUMONE_UNISWAP_ORACLE, multisigPauser: ARBITRUMONE_MULTISIG_PAUSER, keeper: ARBITRUMONE_KEEPER, - monitoredToken: ARBITRUMONE_MONITORED_TOKEN, - monitoredPool: ARBITRUMONE_MONITORED_POOL, - deviationPercent: ARBITRUMONE_DEVIATION_PERCENT, + monitoredMarkets: ARBITRUMONE_MONITORED_MARKETS, } as const; diff --git a/vips/vip-666/addresses/basemainnet.ts b/vips/vip-666/addresses/basemainnet.ts index 07a10280c..545ab2f13 100644 --- a/vips/vip-666/addresses/basemainnet.ts +++ b/vips/vip-666/addresses/basemainnet.ts @@ -19,13 +19,38 @@ export const BASEMAINNET_UNISWAP_ORACLE = ZERO_ADDRESS; export const BASEMAINNET_MULTISIG_PAUSER = ZERO_ADDRESS; export const BASEMAINNET_KEEPER = ZERO_ADDRESS; -// PLACEHOLDER — first monitored token + its Uniswap V3 pool. Leave as ZERO_ADDRESS to skip token wiring. -export const BASEMAINNET_MONITORED_TOKEN = ZERO_ADDRESS; -export const BASEMAINNET_MONITORED_POOL = ZERO_ADDRESS; -export const BASEMAINNET_DEVIATION_PERCENT = 20; - export const BASEMAINNET_DST_CHAIN_ID = LzChainId.basemainnet; +// Eligible Core Pool markets — Aerodrome Slipstream + Uniswap V3 Base sources, +// unified 10% threshold. Aerodrome Slipstream is a Uniswap V3 fork; pools are +// V3-compatible and readable through UniswapOracle.sol's IUniswapV3Pool interface. +export const BASEMAINNET_MONITORED_MARKETS = [ + { + symbol: "WETH", + token: "0x4200000000000000000000000000000000000006", + pool: "0x6c561b446416e1a00e8e93e221854d6ea4171372", // WETH/USDC Uniswap V3 Base + deviationPercent: 10, + }, + { + symbol: "USDC", + token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + pool: "0x6c561b446416e1a00e8e93e221854d6ea4171372", // USDC/WETH Uniswap V3 Base + deviationPercent: 10, + }, + { + symbol: "cbBTC", + token: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + pool: "0x4e962bb3889bf030368f56810a9c96b83cb3e778", // cbBTC/USDC Aerodrome Slipstream + deviationPercent: 10, + }, + { + symbol: "wstETH", + token: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + pool: "0x861a2922be165a5bd41b1e482b49216b465e1b5f", // wstETH/WETH Aerodrome Slipstream + deviationPercent: 10, + }, +]; + export const BASEMAINNET_CONFIG = { name: "Base", dstChainId: BASEMAINNET_DST_CHAIN_ID, @@ -41,7 +66,5 @@ export const BASEMAINNET_CONFIG = { uniswapOracle: BASEMAINNET_UNISWAP_ORACLE, multisigPauser: BASEMAINNET_MULTISIG_PAUSER, keeper: BASEMAINNET_KEEPER, - monitoredToken: BASEMAINNET_MONITORED_TOKEN, - monitoredPool: BASEMAINNET_MONITORED_POOL, - deviationPercent: BASEMAINNET_DEVIATION_PERCENT, + monitoredMarkets: BASEMAINNET_MONITORED_MARKETS, } as const; diff --git a/vips/vip-666/addresses/ethereum.ts b/vips/vip-666/addresses/ethereum.ts index 92e2e1283..8e45c2d08 100644 --- a/vips/vip-666/addresses/ethereum.ts +++ b/vips/vip-666/addresses/ethereum.ts @@ -19,13 +19,75 @@ export const ETHEREUM_UNISWAP_ORACLE = ZERO_ADDRESS; export const ETHEREUM_MULTISIG_PAUSER = ZERO_ADDRESS; export const ETHEREUM_KEEPER = ZERO_ADDRESS; -// PLACEHOLDER — first monitored token + its Uniswap V3 pool. Leave as ZERO_ADDRESS to skip token wiring. -export const ETHEREUM_MONITORED_TOKEN = ZERO_ADDRESS; -export const ETHEREUM_MONITORED_POOL = ZERO_ADDRESS; -export const ETHEREUM_DEVIATION_PERCENT = 20; - export const ETHEREUM_DST_CHAIN_ID = LzChainId.ethereum; +// Eligible Core Pool markets — Uniswap V3 + Curve sources, unified 10% threshold. +// crvUSD and EIGEN are intentionally excluded (per market spec). eBTC source is +// Curve; UniswapOracle.sol cannot read Curve pools, so price reads will fail at +// handleDeviation time until a CurveOracle is deployed and wired separately. +export const ETHEREUM_MONITORED_MARKETS = [ + { + symbol: "WETH", + token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + pool: "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // WETH/USDC Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "WBTC", + token: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + pool: "0x99ac8ca7087fa4a2a1fb6357269965a2014abc35", // WBTC/USDC Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "USDC", + token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + pool: "0x3416cf6c708da44db2624d63ea0aaef7113527c6", // USDC/USDT Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "USDT", + token: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + pool: "0x3416cf6c708da44db2624d63ea0aaef7113527c6", // USDC/USDT Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "LBTC", + token: "0x8236a87084f8B84306f72007F36F2618A5634494", + pool: "0x87428a53e14d24ab19c6ca4939b4df93b8996ca9", // LBTC/WBTC Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "USDe", + token: "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3", + pool: "0xe6d7ebb9f1a9519dc06d557e03c522d53520e76a", // USDe/USDC Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "eBTC", + token: "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", + pool: "0x7704d01908afd31bf647d969c295bb45230cd2d6", // eBTC/WBTC Curve — needs CurveOracle + deviationPercent: 10, + }, + { + symbol: "DAI", + token: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + pool: "0x5777d92f208679db4b9778590fa3cab3ac9e2168", // DAI/USDC Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "tBTC", + token: "0x18084fbA666a33d37592fA2633fD49a74DD93a88", + pool: "0x97944213d2caeea773da1c9b11b0525f25b749cc", // tBTC/WETH Uniswap V3 + deviationPercent: 10, + }, + { + symbol: "USDS", + token: "0xdC035D45d973E3EC169d2276DDab16f1e407384F", + pool: "0xe9f1e2ef814f5686c30ce6fb7103d0f780836c67", // USDS/DAI Uniswap V3 + deviationPercent: 10, + }, +]; + export const ETHEREUM_CONFIG = { name: "Ethereum", dstChainId: ETHEREUM_DST_CHAIN_ID, @@ -41,7 +103,5 @@ export const ETHEREUM_CONFIG = { uniswapOracle: ETHEREUM_UNISWAP_ORACLE, multisigPauser: ETHEREUM_MULTISIG_PAUSER, keeper: ETHEREUM_KEEPER, - monitoredToken: ETHEREUM_MONITORED_TOKEN, - monitoredPool: ETHEREUM_MONITORED_POOL, - deviationPercent: ETHEREUM_DEVIATION_PERCENT, + monitoredMarkets: ETHEREUM_MONITORED_MARKETS, } as const; diff --git a/vips/vip-666/addresses/zksyncmainnet.ts b/vips/vip-666/addresses/zksyncmainnet.ts index ead929164..3e9f0b226 100644 --- a/vips/vip-666/addresses/zksyncmainnet.ts +++ b/vips/vip-666/addresses/zksyncmainnet.ts @@ -19,13 +19,20 @@ export const ZKSYNCMAINNET_UNISWAP_ORACLE = ZERO_ADDRESS; export const ZKSYNCMAINNET_MULTISIG_PAUSER = ZERO_ADDRESS; export const ZKSYNCMAINNET_KEEPER = ZERO_ADDRESS; -// PLACEHOLDER — first monitored token + its DEX pool. Leave as ZERO_ADDRESS to skip token wiring. -export const ZKSYNCMAINNET_MONITORED_TOKEN = ZERO_ADDRESS; -export const ZKSYNCMAINNET_MONITORED_POOL = ZERO_ADDRESS; -export const ZKSYNCMAINNET_DEVIATION_PERCENT = 20; - export const ZKSYNCMAINNET_DST_CHAIN_ID = LzChainId.zksyncmainnet; +// Eligible Core Pool markets — SyncSwap V3 (primary). Only WETH passes the +// liquidity threshold; the rest of the spec's markets sit in "Thin Pool — Needs +// Discussion" and are excluded from this VIP. +export const ZKSYNCMAINNET_MONITORED_MARKETS = [ + { + symbol: "WETH", + token: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91", + pool: "0xe955c98e8411ee4c7332ebe48df7f0ca12711dc2", // WETH/USDC SyncSwap V3 + deviationPercent: 10, + }, +]; + export const ZKSYNCMAINNET_CONFIG = { name: "zkSync Era", dstChainId: ZKSYNCMAINNET_DST_CHAIN_ID, @@ -41,7 +48,5 @@ export const ZKSYNCMAINNET_CONFIG = { uniswapOracle: ZKSYNCMAINNET_UNISWAP_ORACLE, multisigPauser: ZKSYNCMAINNET_MULTISIG_PAUSER, keeper: ZKSYNCMAINNET_KEEPER, - monitoredToken: ZKSYNCMAINNET_MONITORED_TOKEN, - monitoredPool: ZKSYNCMAINNET_MONITORED_POOL, - deviationPercent: ZKSYNCMAINNET_DEVIATION_PERCENT, + monitoredMarkets: ZKSYNCMAINNET_MONITORED_MARKETS, } as const; diff --git a/vips/vip-666/bscmainnet.ts b/vips/vip-666/bscmainnet.ts index 142d2c6c2..1398bf032 100644 --- a/vips/vip-666/bscmainnet.ts +++ b/vips/vip-666/bscmainnet.ts @@ -7,6 +7,14 @@ import { BASEMAINNET_CONFIG } from "./addresses/basemainnet"; import { ETHEREUM_CONFIG } from "./addresses/ethereum"; import { ZKSYNCMAINNET_CONFIG } from "./addresses/zksyncmainnet"; +// Per-chain monitored market: token + DEX pool to read its price + deviation threshold. +export interface MonitoredMarket { + symbol: string; + token: string; + pool: string; + deviationPercent: number; +} + // Per-chain configuration shape. Each chain bundles its ACM, governance accounts, // IL Comptroller, and the EBrake/DeviationSentinel stack addresses (placeholders // until contracts are deployed on the remote chain). @@ -25,9 +33,7 @@ export interface ChainConfig { uniswapOracle: string; multisigPauser: string; keeper: string; - monitoredToken: string; - monitoredPool: string; - deviationPercent: number; + monitoredMarkets: MonitoredMarket[]; } const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, ZKSYNCMAINNET_CONFIG, BASEMAINNET_CONFIG]; @@ -155,27 +161,29 @@ const buildChainCommands = (cfg: ChainConfig) => { ...GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, cfg.multisigPauser, dstChainId)), ]; - // 11. Token-specific wiring. Skipped when no token is yet chosen for the chain - // (monitoredToken == ZERO_ADDRESS) so the proposal stays executable - // even with placeholder addresses. - if (cfg.monitoredToken !== ZERO_ADDRESS) { + // 11. Per-market wiring. For each eligible market, configure the DEX pool on the + // UniswapOracle, point the SentinelOracle at the UniswapOracle for that token, + // then enable deviation monitoring on the DeviationSentinel. Markets with a + // ZERO_ADDRESS token or pool are skipped (placeholder safety). + for (const market of cfg.monitoredMarkets) { + if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; commands.push( { target: cfg.uniswapOracle, signature: "setPoolConfig(address,address)", - params: [cfg.monitoredToken, cfg.monitoredPool], + params: [market.token, market.pool], dstChainId, }, { target: cfg.sentinelOracle, signature: "setTokenOracleConfig(address,address)", - params: [cfg.monitoredToken, cfg.uniswapOracle], + params: [market.token, cfg.uniswapOracle], dstChainId, }, { target: cfg.deviationSentinel, signature: "setTokenConfig(address,(uint8,bool))", - params: [cfg.monitoredToken, [cfg.deviationPercent, true]], + params: [market.token, [market.deviationPercent, true]], dstChainId, }, ); @@ -205,6 +213,7 @@ If approved, this VIP will, for each of Ethereum, Arbitrum One, zkSync Era, and - Grant **Guardian** and governance **Timelocks** the 8 IL-supported EBrake action functions and granular snapshot-reset permissions - Grant the **per-chain 1-of-1 Multisig Pauser** the 8 IL-supported EBrake action functions for manual emergency pausing (Phase 0) - Whitelist Keeper + Guardian + 3 Timelocks as trusted keepers on DeviationSentinel +- Configure deviation monitoring (10% threshold) for the eligible Core Pool markets on each chain — 10 on Ethereum, 5 on Arbitrum One, 4 on Base, 1 on zkSync Era **Permission event summary**: 332 PermissionGranted (83 per chain × 4 chains), 0 PermissionRevoked diff --git a/vips/vip-666/market.md b/vips/vip-666/market.md new file mode 100644 index 000000000..46f417af3 --- /dev/null +++ b/vips/vip-666/market.md @@ -0,0 +1,132 @@ +Step 1 expansion to **Ethereum / Arbitrum One / Base / zkSync** Core Pool markets. Chain-specific primary DEX with Curve as secondary source on Ethereum (crvUSD, eBTC). Unified **10%** deviation threshold. Isolated / Curve / LSE / Ethena pools out of scope. Pool data: GeckoTerminal API, 2026-04-27. + +--- + +## Proposed Market List + +### Ethereum + +**Primary DEX:** `uniswap_v3` (secondary: `curve` for crvUSD / eBTC) + +#### Eligible — 12 markets + +| **#** | **Token** | **Pool** | **Pool Address** | **TVL** | **24h Vol** | **Source** | **Threshold** | +| ----- | ---------- | ----------- | -------------------------------------------- | ------- | ----------- | ---------- | ------------- | +| 1 | WETH | WETH/USDC | `0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640` | $101.5M | $37.4M | Uniswap V3 | 10% | +| 2 | ~~crvUSD~~ | crvUSD/USDC | `0x4dece678ceceb27446b35c672dc7d61f30bad69e` | $84.4M | $13.4M | Curve | 10% | +| 3 | WBTC | WBTC/USDC | `0x99ac8ca7087fa4a2a1fb6357269965a2014abc35` | $30.3M | $2.4M | Uniswap V3 | 10% | +| 4 | USDC | USDC/USDT | `0x3416cf6c708da44db2624d63ea0aaef7113527c6` | $19.1M | $4.6M | Uniswap V3 | 10% | +| 5 | USDT | USDT/USDC | `0x3416cf6c708da44db2624d63ea0aaef7113527c6` | $19.1M | $4.6M | Uniswap V3 | 10% | +| 6 | LBTC | LBTC/WBTC | `0x87428a53e14d24ab19c6ca4939b4df93b8996ca9` | $7.0M | $48K | Uniswap V3 | 10% | +| 7 | USDe | USDe/USDC | `0xe6d7ebb9f1a9519dc06d557e03c522d53520e76a` | $2.4M | $1.6M | Uniswap V3 | 10% | +| 8 | eBTC | eBTC/WBTC | `0x7704d01908afd31bf647d969c295bb45230cd2d6` | $1.59M | $108K | Curve | 10% | +| 9 | DAI | DAI/USDC | `0x5777d92f208679db4b9778590fa3cab3ac9e2168` | $1.1M | $3.3M | Uniswap V3 | 10% | +| 10 | ~~EIGEN~~ | EIGEN/USDC | `0xd640333b71b015092d9b3afcff3e427036304370` | $1.1M | $22K | Uniswap V3 | 10% | +| 11 | tBTC | tBTC/WETH | `0x97944213d2caeea773da1c9b11b0525f25b749cc` | $263K | $3K | Uniswap V3 | 10% | +| 12 | USDS | USDS/DAI | `0xe9f1e2ef814f5686c30ce6fb7103d0f780836c67` | $209K | $298 | Uniswap V3 | 10% | + +#### Thin Pool — Needs Discussion — 2 markets + +| **Token** | **Best Pool** | **Pool Address** | **TVL** | **Source** | **Notes** | +| ---------- | ------------- | -------------------------------------------- | ------- | ---------- | --------------------------- | +| ~~TUSD~~ | TUSD/USDC | `0x39529e96c28807655b5856b3d342c6225111770e` | $518 | Uniswap V3 | Token in run-off | +| ~~weETHs~~ | weETHs/WETH | `0x174eff1363c4b446f3425315bd6c12f305823d6a` | $0 | Uniswap V3 | Quote-matched but near-zero | + +#### Not Eligible — 3 markets + +| **Token** | **Reason** | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ~~BAL~~ | Primary liquidity on **Balancer V2** (BAL/WETH 1% @ $5.9M); UniV3 / Curve pools have no usable depth — would need Balancer adapter | +| sUSDe | ERC4626 vault — no on-chain pool with quote = USDe | +| sUSDS | ERC4626 vault — no on-chain pool with quote = USDS | + +**Ethereum summary:** 12 eligible / 2 thin / 3 not eligible (17 total) + +--- + +### Arbitrum One + +**Primary DEX:** `uniswap_v3_arbitrum` (fallback: `camelot-v3`) + +#### Eligible — 5 markets + +| **#** | **Token** | **Pool** | **Pool Address** | **TVL** | **24h Vol** | **Source** | **Threshold** | +| ----- | --------- | ---------- | -------------------------------------------- | ------- | ----------- | ---------- | ------------- | +| 1 | WETH | WETH/USDC | `0xc6962004f452be9203591991d15f6b388e09e8d0` | $55.3M | $71.1M | Uniswap V3 | 10% | +| 2 | WBTC | WBTC/USDC | `0x0e4831319a50228b9e450861297ab92dee15b44f` | $8.2M | $7.3M | Uniswap V3 | 10% | +| 3 | USDC | USDC/USDT | `0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6` | $2.6M | $1.3M | Uniswap V3 | 10% | +| 4 | USD₮0 | USD₮0/USDC | `0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6` | $2.6M | $1.3M | Uniswap V3 | 10% | +| 5 | ARB | ARB/USDC | `0xaebdca1bc8d89177ebe2308d62af5e74885dccc3` | $675K | $81K | Uniswap V3 | 10% | + +#### Not Eligible — 2 markets + +| **Token** | **Reason** | +| ----------- | ----------------------------------------------------------- | +| gmBTC-USDC | GMX market token (GM) — synthetic, no DEX V3 pool by design | +| gmWETH-USDC | GMX market token (GM) — synthetic, no DEX V3 pool by design | + +**Arbitrum One summary:** 5 eligible / 0 thin / 2 not eligible (7 total) + +--- + +### Base + +**Primary DEX:** `aerodrome-slipstream` (fallback: `uniswap-v3-base`) + +#### Eligible — 4 markets + +| **#** | **Token** | **Pool** | **Pool Address** | **TVL** | **24h Vol** | **Source** | **Threshold** | +| ----- | --------- | ----------- | -------------------------------------------- | ------- | ----------- | -------------------- | ------------- | +| 1 | WETH | WETH/USDC | `0x6c561b446416e1a00e8e93e221854d6ea4171372` | $124.4M | $70.1M | Uniswap V3 Base | 10% | +| 2 | USDC | USDC/WETH | `0x6c561b446416e1a00e8e93e221854d6ea4171372` | $124.2M | $70.1M | Uniswap V3 Base | 10% | +| 3 | cbBTC | cbBTC/USDC | `0x4e962bb3889bf030368f56810a9c96b83cb3e778` | $12.4M | $111.9M | Aerodrome Slipstream | 10% | +| 4 | wstETH | wstETH/WETH | `0x861a2922be165a5bd41b1e482b49216b465e1b5f` | $1.5M | $2.9M | Aerodrome Slipstream | 10% | + +#### Not Eligible — 1 market + +| **Token** | **Reason** | +| --------------- | -------------------------------------------------------------- | +| ~~wsuperOETHb~~ | No pool with quote = WETH on Aerodrome Slipstream / UniV3 Base | + +**Base summary:** 4 eligible / 0 thin / 1 not eligible (5 total) + +--- + +### zkSync + +**Primary DEX:** `syncswap-v3-zksync` (fallback: `pancakeswap-v3-zksync`) + +⚠️ zkSync DEX liquidity is materially shallower than BSC. Only WETH/USDC passes the $200K threshold. The remaining markets are listed under Thin Pool for review. + +#### Eligible — 1 market + +| **#** | **Token** | **Pool** | **Pool Address** | **TVL** | **24h Vol** | **Source** | **Threshold** | +| ----- | --------- | --------- | -------------------------------------------- | ------- | ----------- | ----------- | ------------- | +| 1 | WETH | WETH/USDC | `0xe955c98e8411ee4c7332ebe48df7f0ca12711dc2` | $491K | $2K | SyncSwap V3 | 10% | + +#### Thin Pool — Needs Discussion — 8 markets + +| **Token** | **Best Pool** | **Pool Address** | **TVL** | **Source** | **Notes** | +| --------- | ------------- | -------------------------------------------- | ------- | -------------- | --------------------------- | +| USDC.e | USDC.e/USDC | `0x3aef05a8e7d7a83f5527eded214e0b24a87d0991` | $35K | PancakeSwap V3 | Thin | +| wstETH | wstETH/WETH | `0xb249b76c7bda837b8a507a0e12caeda90d25d32f` | $25K | SyncSwap V3 | Quote-matched but thin | +| WBTC | WBTC/WETH | `0x9cb8b12cb0223e105155318b72addda15d588fb9` | $12K | PancakeSwap V3 | Thin | +| USDC | USDC/USDT | `0xd05eef3792276e92bb051029dadfc2bf81121692` | $9K | PancakeSwap V3 | Thin | +| USDT | USDT/USDC | `0xd05eef3792276e92bb051029dadfc2bf81121692` | $9K | PancakeSwap V3 | Thin | +| ZK | ZK/USDC | `0xfda764f84168ed39b4f3e1c14a59f21a4660d883` | $3K | PancakeSwap V3 | Thin | +| ~~zkETH~~ | zkETH/WETH | `0xb2d87ed6814f519521172b04b5e6a4f77c50ded3` | $127 | PancakeSwap V3 | Quote-matched but near-zero | +| ~~wUSDM~~ | wUSDM/USDC | `0xc9d2f9f56904dd71de34f2d696f5afc508f93ac3` | $103 | SyncSwap V3 | Near-zero | + +**zkSync summary:** 1 eligible / 8 thin / 0 not eligible (9 total) + +--- + +## Aggregate Summary + +| **Chain** | **Source(s)** | **Eligible** | **Thin** | **Not Eligible** | **Total** | +| ------------ | --------------------------------- | ------------ | -------- | ---------------- | --------- | +| Ethereum | Uniswap V3 + Curve | 12 | 2 | 3 | 17 | +| Arbitrum One | Uniswap V3 + Camelot V3 | 5 | 0 | 2 | 7 | +| Base | Aerodrome Slipstream + UniV3 Base | 4 | 0 | 1 | 5 | +| zkSync | SyncSwap V3 + PancakeSwap V3 | 1 | 8 | 0 | 9 | +| **Total** | — | **22** | **10** | **6** | **38** | From 4dd29e946c3c29d5e8ae638b229391192fa75f68 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 28 Apr 2026 19:36:42 +0530 Subject: [PATCH 03/15] chore: drop zkSync Era from scope --- simulations/vip-666/zksyncmainnet.ts | 11 ------ vips/vip-666/addresses/zksyncmainnet.ts | 52 ------------------------- 2 files changed, 63 deletions(-) delete mode 100644 simulations/vip-666/zksyncmainnet.ts delete mode 100644 vips/vip-666/addresses/zksyncmainnet.ts diff --git a/simulations/vip-666/zksyncmainnet.ts b/simulations/vip-666/zksyncmainnet.ts deleted file mode 100644 index 970551fc4..000000000 --- a/simulations/vip-666/zksyncmainnet.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { forking } from "src/vip-framework"; - -import { ZKSYNCMAINNET_CONFIG } from "../../vips/vip-666/addresses/zksyncmainnet"; -import { runVip666Suite } from "./shared"; - -// TODO: set fork block once the EBrake/DeviationSentinel stack is deployed on zkSync Era. -const FORK_BLOCK = 0; - -forking(FORK_BLOCK, async () => { - await runVip666Suite(ZKSYNCMAINNET_CONFIG); -}); diff --git a/vips/vip-666/addresses/zksyncmainnet.ts b/vips/vip-666/addresses/zksyncmainnet.ts deleted file mode 100644 index 3e9f0b226..000000000 --- a/vips/vip-666/addresses/zksyncmainnet.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; -import { LzChainId } from "src/types"; - -const { zksyncmainnet } = NETWORK_ADDRESSES; - -// Already-deployed governance + protocol addresses on zkSync Era mainnet -export const ZKSYNCMAINNET_ACM = zksyncmainnet.ACCESS_CONTROL_MANAGER; -export const ZKSYNCMAINNET_GUARDIAN = zksyncmainnet.GUARDIAN; -export const ZKSYNCMAINNET_NORMAL_TIMELOCK = zksyncmainnet.NORMAL_TIMELOCK; -export const ZKSYNCMAINNET_FAST_TRACK_TIMELOCK = zksyncmainnet.FAST_TRACK_TIMELOCK; -export const ZKSYNCMAINNET_CRITICAL_TIMELOCK = zksyncmainnet.CRITICAL_TIMELOCK; -export const ZKSYNCMAINNET_CORE_COMPTROLLER = zksyncmainnet.CORE_COMPTROLLER; - -// PLACEHOLDERS — to be filled once the EBrake/DeviationSentinel stack is deployed on zkSync Era -export const ZKSYNCMAINNET_DEVIATION_SENTINEL = ZERO_ADDRESS; -export const ZKSYNCMAINNET_EBRAKE = ZERO_ADDRESS; -export const ZKSYNCMAINNET_SENTINEL_ORACLE = ZERO_ADDRESS; -export const ZKSYNCMAINNET_UNISWAP_ORACLE = ZERO_ADDRESS; -export const ZKSYNCMAINNET_MULTISIG_PAUSER = ZERO_ADDRESS; -export const ZKSYNCMAINNET_KEEPER = ZERO_ADDRESS; - -export const ZKSYNCMAINNET_DST_CHAIN_ID = LzChainId.zksyncmainnet; - -// Eligible Core Pool markets — SyncSwap V3 (primary). Only WETH passes the -// liquidity threshold; the rest of the spec's markets sit in "Thin Pool — Needs -// Discussion" and are excluded from this VIP. -export const ZKSYNCMAINNET_MONITORED_MARKETS = [ - { - symbol: "WETH", - token: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91", - pool: "0xe955c98e8411ee4c7332ebe48df7f0ca12711dc2", // WETH/USDC SyncSwap V3 - deviationPercent: 10, - }, -]; - -export const ZKSYNCMAINNET_CONFIG = { - name: "zkSync Era", - dstChainId: ZKSYNCMAINNET_DST_CHAIN_ID, - acm: ZKSYNCMAINNET_ACM, - guardian: ZKSYNCMAINNET_GUARDIAN, - normalTimelock: ZKSYNCMAINNET_NORMAL_TIMELOCK, - fastTrackTimelock: ZKSYNCMAINNET_FAST_TRACK_TIMELOCK, - criticalTimelock: ZKSYNCMAINNET_CRITICAL_TIMELOCK, - comptroller: ZKSYNCMAINNET_CORE_COMPTROLLER, - deviationSentinel: ZKSYNCMAINNET_DEVIATION_SENTINEL, - eBrake: ZKSYNCMAINNET_EBRAKE, - sentinelOracle: ZKSYNCMAINNET_SENTINEL_ORACLE, - uniswapOracle: ZKSYNCMAINNET_UNISWAP_ORACLE, - multisigPauser: ZKSYNCMAINNET_MULTISIG_PAUSER, - keeper: ZKSYNCMAINNET_KEEPER, - monitoredMarkets: ZKSYNCMAINNET_MONITORED_MARKETS, -} as const; From a19fbf44ebd825051b345e38a2f31f172fd28a17 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 28 Apr 2026 19:41:07 +0530 Subject: [PATCH 04/15] feat: fill deployed contract addresses for Ethereum, Arbitrum One, Base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeviationSentinel, EBrake, SentinelOracle, and UniswapOracle proxies sourced from venus-periphery PR #65 (feat/VPD-1134). Keeper and multisig pauser reuse the same BSC mainnet addresses (keeper: 0x57fa…, multisig: 0xCCa5…). --- vips/vip-666/addresses/arbitrumone.ts | 14 +++++++------- vips/vip-666/addresses/basemainnet.ts | 16 ++++++++-------- vips/vip-666/addresses/ethereum.ts | 16 ++++++++-------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/vips/vip-666/addresses/arbitrumone.ts b/vips/vip-666/addresses/arbitrumone.ts index a2faa49af..418e59e46 100644 --- a/vips/vip-666/addresses/arbitrumone.ts +++ b/vips/vip-666/addresses/arbitrumone.ts @@ -11,13 +11,13 @@ export const ARBITRUMONE_FAST_TRACK_TIMELOCK = arbitrumone.FAST_TRACK_TIMELOCK; export const ARBITRUMONE_CRITICAL_TIMELOCK = arbitrumone.CRITICAL_TIMELOCK; export const ARBITRUMONE_CORE_COMPTROLLER = arbitrumone.CORE_COMPTROLLER; -// PLACEHOLDERS — to be filled once the EBrake/DeviationSentinel stack is deployed on Arbitrum One -export const ARBITRUMONE_DEVIATION_SENTINEL = ZERO_ADDRESS; -export const ARBITRUMONE_EBRAKE = ZERO_ADDRESS; -export const ARBITRUMONE_SENTINEL_ORACLE = ZERO_ADDRESS; -export const ARBITRUMONE_UNISWAP_ORACLE = ZERO_ADDRESS; -export const ARBITRUMONE_MULTISIG_PAUSER = ZERO_ADDRESS; -export const ARBITRUMONE_KEEPER = ZERO_ADDRESS; +// Deployed via venus-periphery PR #65 (feat/VPD-1134) +export const ARBITRUMONE_DEVIATION_SENTINEL = "0xb4CC54B33d34fD809E8fBD83A066158591ED7Fba"; +export const ARBITRUMONE_EBRAKE = "0xFc4CE7Ca9BB5119705Cfb84d6e4476e8a4032b26"; +export const ARBITRUMONE_SENTINEL_ORACLE = "0x3563CAbc541a0432C66A64942ffB4070a9726226"; +export const ARBITRUMONE_UNISWAP_ORACLE = "0xB6CFbfe6834EF519f002DBc1a8B81Ea437Ca647D"; +export const ARBITRUMONE_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig +export const ARBITRUMONE_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; export const ARBITRUMONE_DST_CHAIN_ID = LzChainId.arbitrumone; diff --git a/vips/vip-666/addresses/basemainnet.ts b/vips/vip-666/addresses/basemainnet.ts index 545ab2f13..fbb38a77f 100644 --- a/vips/vip-666/addresses/basemainnet.ts +++ b/vips/vip-666/addresses/basemainnet.ts @@ -1,4 +1,4 @@ -import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { LzChainId } from "src/types"; const { basemainnet } = NETWORK_ADDRESSES; @@ -11,13 +11,13 @@ export const BASEMAINNET_FAST_TRACK_TIMELOCK = basemainnet.FAST_TRACK_TIMELOCK; export const BASEMAINNET_CRITICAL_TIMELOCK = basemainnet.CRITICAL_TIMELOCK; export const BASEMAINNET_CORE_COMPTROLLER = basemainnet.CORE_COMPTROLLER; -// PLACEHOLDERS — to be filled once the EBrake/DeviationSentinel stack is deployed on Base -export const BASEMAINNET_DEVIATION_SENTINEL = ZERO_ADDRESS; -export const BASEMAINNET_EBRAKE = ZERO_ADDRESS; -export const BASEMAINNET_SENTINEL_ORACLE = ZERO_ADDRESS; -export const BASEMAINNET_UNISWAP_ORACLE = ZERO_ADDRESS; -export const BASEMAINNET_MULTISIG_PAUSER = ZERO_ADDRESS; -export const BASEMAINNET_KEEPER = ZERO_ADDRESS; +// Deployed via venus-periphery PR #65 (feat/VPD-1134) +export const BASEMAINNET_DEVIATION_SENTINEL = "0x12D09d5b13A673269cdB624D17A42f45a5233076"; +export const BASEMAINNET_EBRAKE = "0x062C68Af7B9Fb059DCB7FA4B6b92E633350fb7c2"; +export const BASEMAINNET_SENTINEL_ORACLE = "0xCdD6D79Fd313C21967CED04C1b8bE70BDc27574D"; +export const BASEMAINNET_UNISWAP_ORACLE = "0xc3b5169a7d5f6341403c74187Db3C4Fe6d447762"; +export const BASEMAINNET_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig +export const BASEMAINNET_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; export const BASEMAINNET_DST_CHAIN_ID = LzChainId.basemainnet; diff --git a/vips/vip-666/addresses/ethereum.ts b/vips/vip-666/addresses/ethereum.ts index 8e45c2d08..1b4378add 100644 --- a/vips/vip-666/addresses/ethereum.ts +++ b/vips/vip-666/addresses/ethereum.ts @@ -1,4 +1,4 @@ -import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { LzChainId } from "src/types"; const { ethereum } = NETWORK_ADDRESSES; @@ -11,13 +11,13 @@ export const ETHEREUM_FAST_TRACK_TIMELOCK = ethereum.FAST_TRACK_TIMELOCK; export const ETHEREUM_CRITICAL_TIMELOCK = ethereum.CRITICAL_TIMELOCK; export const ETHEREUM_CORE_COMPTROLLER = ethereum.CORE_COMPTROLLER; -// PLACEHOLDERS — to be filled once the EBrake/DeviationSentinel stack is deployed on Ethereum -export const ETHEREUM_DEVIATION_SENTINEL = ZERO_ADDRESS; -export const ETHEREUM_EBRAKE = ZERO_ADDRESS; -export const ETHEREUM_SENTINEL_ORACLE = ZERO_ADDRESS; -export const ETHEREUM_UNISWAP_ORACLE = ZERO_ADDRESS; -export const ETHEREUM_MULTISIG_PAUSER = ZERO_ADDRESS; -export const ETHEREUM_KEEPER = ZERO_ADDRESS; +// Deployed via venus-periphery PR #65 (feat/VPD-1134) +export const ETHEREUM_DEVIATION_SENTINEL = "0x7D0EFA41eBF1aF242A37174E1E047bD6ea1b1B9c"; +export const ETHEREUM_EBRAKE = "0xCD09042c5DFFed762998Df9a058ec5944e39949B"; +export const ETHEREUM_SENTINEL_ORACLE = "0x444C53E194B40c272fAd683210e2cB1c16Ab132e"; +export const ETHEREUM_UNISWAP_ORACLE = "0x873993F8f5f5Ddbae0952e939ab3005Af363Af00"; +export const ETHEREUM_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig +export const ETHEREUM_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; export const ETHEREUM_DST_CHAIN_ID = LzChainId.ethereum; From 6b62a244ef487cae5dfa3397b4f8bdfacd4655ab Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 29 Apr 2026 08:24:40 +0530 Subject: [PATCH 05/15] chore(vip-666): remove zkSync Era from scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove ZKSYNCMAINNET_CONFIG import and NETWORKS array entry - Update proposal title from 4 chains to 3 (Ethereum, Arbitrum One, Base) - Update description and summary to reflect 3-chain deployment - Reduce permission event count from 332 to 249 (83 per chain × 3) - Update market count line: remove zkSync market count reference - Set realistic fork block numbers for remaining 3 chains - Delete market.md (was scoped to 4-chain proposal) --- simulations/vip-666/arbitrumone.ts | 3 +- simulations/vip-666/basemainnet.ts | 3 +- simulations/vip-666/ethereum.ts | 3 +- vips/vip-666/bscmainnet.ts | 13 ++- vips/vip-666/market.md | 132 ----------------------------- 5 files changed, 9 insertions(+), 145 deletions(-) delete mode 100644 vips/vip-666/market.md diff --git a/simulations/vip-666/arbitrumone.ts b/simulations/vip-666/arbitrumone.ts index 18b28ef84..e940a72d7 100644 --- a/simulations/vip-666/arbitrumone.ts +++ b/simulations/vip-666/arbitrumone.ts @@ -3,8 +3,7 @@ import { forking } from "src/vip-framework"; import { ARBITRUMONE_CONFIG } from "../../vips/vip-666/addresses/arbitrumone"; import { runVip666Suite } from "./shared"; -// TODO: set fork block once the EBrake/DeviationSentinel stack is deployed on Arbitrum One. -const FORK_BLOCK = 0; +const FORK_BLOCK = 457434103; forking(FORK_BLOCK, async () => { await runVip666Suite(ARBITRUMONE_CONFIG); diff --git a/simulations/vip-666/basemainnet.ts b/simulations/vip-666/basemainnet.ts index f877da806..bb7625916 100644 --- a/simulations/vip-666/basemainnet.ts +++ b/simulations/vip-666/basemainnet.ts @@ -3,8 +3,7 @@ import { forking } from "src/vip-framework"; import { BASEMAINNET_CONFIG } from "../../vips/vip-666/addresses/basemainnet"; import { runVip666Suite } from "./shared"; -// TODO: set fork block once the EBrake/DeviationSentinel stack is deployed on Base. -const FORK_BLOCK = 0; +const FORK_BLOCK = 45320649; forking(FORK_BLOCK, async () => { await runVip666Suite(BASEMAINNET_CONFIG); diff --git a/simulations/vip-666/ethereum.ts b/simulations/vip-666/ethereum.ts index 6b9c591fb..406522237 100644 --- a/simulations/vip-666/ethereum.ts +++ b/simulations/vip-666/ethereum.ts @@ -3,8 +3,7 @@ import { forking } from "src/vip-framework"; import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; import { runVip666Suite } from "./shared"; -// TODO: set fork block once the EBrake/DeviationSentinel stack is deployed on Ethereum. -const FORK_BLOCK = 0; +const FORK_BLOCK = 24982817; forking(FORK_BLOCK, async () => { await runVip666Suite(ETHEREUM_CONFIG); diff --git a/vips/vip-666/bscmainnet.ts b/vips/vip-666/bscmainnet.ts index 1398bf032..0f8fd3953 100644 --- a/vips/vip-666/bscmainnet.ts +++ b/vips/vip-666/bscmainnet.ts @@ -5,7 +5,6 @@ import { makeProposal } from "src/utils"; import { ARBITRUMONE_CONFIG } from "./addresses/arbitrumone"; import { BASEMAINNET_CONFIG } from "./addresses/basemainnet"; import { ETHEREUM_CONFIG } from "./addresses/ethereum"; -import { ZKSYNCMAINNET_CONFIG } from "./addresses/zksyncmainnet"; // Per-chain monitored market: token + DEX pool to read its price + deviation threshold. export interface MonitoredMarket { @@ -36,7 +35,7 @@ export interface ChainConfig { monitoredMarkets: MonitoredMarket[]; } -const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, ZKSYNCMAINNET_CONFIG, BASEMAINNET_CONFIG]; +const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, BASEMAINNET_CONFIG]; // DeviationSentinel access-controlled functions (3 total — setTokenConfig uses // the struct-tuple form (uint8,bool) verbatim, matching DeviationSentinel.sol). @@ -195,16 +194,16 @@ const buildChainCommands = (cfg: ChainConfig) => { export const vip666 = () => { const meta = { version: "v2", - title: "VIP-666 [Ethereum, Arbitrum One, zkSync Era, Base] Configure DeviationSentinel + EBrakeV2", + title: "VIP-666 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2", description: `#### Description -This VIP configures the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, **zkSync Era**, and **Base**, mirroring the BSC setup from VIP-590 + VIP-610. Each chain's DeviationSentinel routes automated oracle-deviation enforcement through a local EBrakeV2, which applies per-action, per-market restrictions (pause borrow/supply, zero collateral factor) without manual intervention. +This VIP configures the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, and **Base**, mirroring the BSC setup from VIP-590 + VIP-610. Each chain's DeviationSentinel routes automated oracle-deviation enforcement through a local EBrakeV2, which applies per-action, per-market restrictions (pause borrow/supply, zero collateral factor) without manual intervention. Because EBrake on these chains uses \`isIsolatedPool=true\` (single-pool IL Comptroller, not the BSC Diamond), only the IL-supported subset of EBrake action functions is granted. Diamond-only functions (\`pauseFlashLoan\`, \`disablePoolBorrow\`, \`revokeFlashLoanAccess\`, \`decreaseCF(address,uint96,uint256)\`) are omitted as they revert on IL comptrollers. #### Summary -If approved, this VIP will, for each of Ethereum, Arbitrum One, zkSync Era, and Base: +If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: - Accept governance ownership of the **DeviationSentinel**, **SentinelOracle**, **UniswapOracle**, and **EBrakeV2** contracts - Grant admin permissions on DeviationSentinel, SentinelOracle, and UniswapOracle to Guardian + 3 Timelocks @@ -213,9 +212,9 @@ If approved, this VIP will, for each of Ethereum, Arbitrum One, zkSync Era, and - Grant **Guardian** and governance **Timelocks** the 8 IL-supported EBrake action functions and granular snapshot-reset permissions - Grant the **per-chain 1-of-1 Multisig Pauser** the 8 IL-supported EBrake action functions for manual emergency pausing (Phase 0) - Whitelist Keeper + Guardian + 3 Timelocks as trusted keepers on DeviationSentinel -- Configure deviation monitoring (10% threshold) for the eligible Core Pool markets on each chain — 10 on Ethereum, 5 on Arbitrum One, 4 on Base, 1 on zkSync Era +- Configure deviation monitoring (10% threshold) for the eligible Core Pool markets on each chain — 10 on Ethereum, 5 on Arbitrum One, 4 on Base -**Permission event summary**: 332 PermissionGranted (83 per chain × 4 chains), 0 PermissionRevoked +**Permission event summary**: 249 PermissionGranted (83 per chain × 3 chains), 0 PermissionRevoked #### References diff --git a/vips/vip-666/market.md b/vips/vip-666/market.md deleted file mode 100644 index 46f417af3..000000000 --- a/vips/vip-666/market.md +++ /dev/null @@ -1,132 +0,0 @@ -Step 1 expansion to **Ethereum / Arbitrum One / Base / zkSync** Core Pool markets. Chain-specific primary DEX with Curve as secondary source on Ethereum (crvUSD, eBTC). Unified **10%** deviation threshold. Isolated / Curve / LSE / Ethena pools out of scope. Pool data: GeckoTerminal API, 2026-04-27. - ---- - -## Proposed Market List - -### Ethereum - -**Primary DEX:** `uniswap_v3` (secondary: `curve` for crvUSD / eBTC) - -#### Eligible — 12 markets - -| **#** | **Token** | **Pool** | **Pool Address** | **TVL** | **24h Vol** | **Source** | **Threshold** | -| ----- | ---------- | ----------- | -------------------------------------------- | ------- | ----------- | ---------- | ------------- | -| 1 | WETH | WETH/USDC | `0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640` | $101.5M | $37.4M | Uniswap V3 | 10% | -| 2 | ~~crvUSD~~ | crvUSD/USDC | `0x4dece678ceceb27446b35c672dc7d61f30bad69e` | $84.4M | $13.4M | Curve | 10% | -| 3 | WBTC | WBTC/USDC | `0x99ac8ca7087fa4a2a1fb6357269965a2014abc35` | $30.3M | $2.4M | Uniswap V3 | 10% | -| 4 | USDC | USDC/USDT | `0x3416cf6c708da44db2624d63ea0aaef7113527c6` | $19.1M | $4.6M | Uniswap V3 | 10% | -| 5 | USDT | USDT/USDC | `0x3416cf6c708da44db2624d63ea0aaef7113527c6` | $19.1M | $4.6M | Uniswap V3 | 10% | -| 6 | LBTC | LBTC/WBTC | `0x87428a53e14d24ab19c6ca4939b4df93b8996ca9` | $7.0M | $48K | Uniswap V3 | 10% | -| 7 | USDe | USDe/USDC | `0xe6d7ebb9f1a9519dc06d557e03c522d53520e76a` | $2.4M | $1.6M | Uniswap V3 | 10% | -| 8 | eBTC | eBTC/WBTC | `0x7704d01908afd31bf647d969c295bb45230cd2d6` | $1.59M | $108K | Curve | 10% | -| 9 | DAI | DAI/USDC | `0x5777d92f208679db4b9778590fa3cab3ac9e2168` | $1.1M | $3.3M | Uniswap V3 | 10% | -| 10 | ~~EIGEN~~ | EIGEN/USDC | `0xd640333b71b015092d9b3afcff3e427036304370` | $1.1M | $22K | Uniswap V3 | 10% | -| 11 | tBTC | tBTC/WETH | `0x97944213d2caeea773da1c9b11b0525f25b749cc` | $263K | $3K | Uniswap V3 | 10% | -| 12 | USDS | USDS/DAI | `0xe9f1e2ef814f5686c30ce6fb7103d0f780836c67` | $209K | $298 | Uniswap V3 | 10% | - -#### Thin Pool — Needs Discussion — 2 markets - -| **Token** | **Best Pool** | **Pool Address** | **TVL** | **Source** | **Notes** | -| ---------- | ------------- | -------------------------------------------- | ------- | ---------- | --------------------------- | -| ~~TUSD~~ | TUSD/USDC | `0x39529e96c28807655b5856b3d342c6225111770e` | $518 | Uniswap V3 | Token in run-off | -| ~~weETHs~~ | weETHs/WETH | `0x174eff1363c4b446f3425315bd6c12f305823d6a` | $0 | Uniswap V3 | Quote-matched but near-zero | - -#### Not Eligible — 3 markets - -| **Token** | **Reason** | -| --------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| ~~BAL~~ | Primary liquidity on **Balancer V2** (BAL/WETH 1% @ $5.9M); UniV3 / Curve pools have no usable depth — would need Balancer adapter | -| sUSDe | ERC4626 vault — no on-chain pool with quote = USDe | -| sUSDS | ERC4626 vault — no on-chain pool with quote = USDS | - -**Ethereum summary:** 12 eligible / 2 thin / 3 not eligible (17 total) - ---- - -### Arbitrum One - -**Primary DEX:** `uniswap_v3_arbitrum` (fallback: `camelot-v3`) - -#### Eligible — 5 markets - -| **#** | **Token** | **Pool** | **Pool Address** | **TVL** | **24h Vol** | **Source** | **Threshold** | -| ----- | --------- | ---------- | -------------------------------------------- | ------- | ----------- | ---------- | ------------- | -| 1 | WETH | WETH/USDC | `0xc6962004f452be9203591991d15f6b388e09e8d0` | $55.3M | $71.1M | Uniswap V3 | 10% | -| 2 | WBTC | WBTC/USDC | `0x0e4831319a50228b9e450861297ab92dee15b44f` | $8.2M | $7.3M | Uniswap V3 | 10% | -| 3 | USDC | USDC/USDT | `0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6` | $2.6M | $1.3M | Uniswap V3 | 10% | -| 4 | USD₮0 | USD₮0/USDC | `0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6` | $2.6M | $1.3M | Uniswap V3 | 10% | -| 5 | ARB | ARB/USDC | `0xaebdca1bc8d89177ebe2308d62af5e74885dccc3` | $675K | $81K | Uniswap V3 | 10% | - -#### Not Eligible — 2 markets - -| **Token** | **Reason** | -| ----------- | ----------------------------------------------------------- | -| gmBTC-USDC | GMX market token (GM) — synthetic, no DEX V3 pool by design | -| gmWETH-USDC | GMX market token (GM) — synthetic, no DEX V3 pool by design | - -**Arbitrum One summary:** 5 eligible / 0 thin / 2 not eligible (7 total) - ---- - -### Base - -**Primary DEX:** `aerodrome-slipstream` (fallback: `uniswap-v3-base`) - -#### Eligible — 4 markets - -| **#** | **Token** | **Pool** | **Pool Address** | **TVL** | **24h Vol** | **Source** | **Threshold** | -| ----- | --------- | ----------- | -------------------------------------------- | ------- | ----------- | -------------------- | ------------- | -| 1 | WETH | WETH/USDC | `0x6c561b446416e1a00e8e93e221854d6ea4171372` | $124.4M | $70.1M | Uniswap V3 Base | 10% | -| 2 | USDC | USDC/WETH | `0x6c561b446416e1a00e8e93e221854d6ea4171372` | $124.2M | $70.1M | Uniswap V3 Base | 10% | -| 3 | cbBTC | cbBTC/USDC | `0x4e962bb3889bf030368f56810a9c96b83cb3e778` | $12.4M | $111.9M | Aerodrome Slipstream | 10% | -| 4 | wstETH | wstETH/WETH | `0x861a2922be165a5bd41b1e482b49216b465e1b5f` | $1.5M | $2.9M | Aerodrome Slipstream | 10% | - -#### Not Eligible — 1 market - -| **Token** | **Reason** | -| --------------- | -------------------------------------------------------------- | -| ~~wsuperOETHb~~ | No pool with quote = WETH on Aerodrome Slipstream / UniV3 Base | - -**Base summary:** 4 eligible / 0 thin / 1 not eligible (5 total) - ---- - -### zkSync - -**Primary DEX:** `syncswap-v3-zksync` (fallback: `pancakeswap-v3-zksync`) - -⚠️ zkSync DEX liquidity is materially shallower than BSC. Only WETH/USDC passes the $200K threshold. The remaining markets are listed under Thin Pool for review. - -#### Eligible — 1 market - -| **#** | **Token** | **Pool** | **Pool Address** | **TVL** | **24h Vol** | **Source** | **Threshold** | -| ----- | --------- | --------- | -------------------------------------------- | ------- | ----------- | ----------- | ------------- | -| 1 | WETH | WETH/USDC | `0xe955c98e8411ee4c7332ebe48df7f0ca12711dc2` | $491K | $2K | SyncSwap V3 | 10% | - -#### Thin Pool — Needs Discussion — 8 markets - -| **Token** | **Best Pool** | **Pool Address** | **TVL** | **Source** | **Notes** | -| --------- | ------------- | -------------------------------------------- | ------- | -------------- | --------------------------- | -| USDC.e | USDC.e/USDC | `0x3aef05a8e7d7a83f5527eded214e0b24a87d0991` | $35K | PancakeSwap V3 | Thin | -| wstETH | wstETH/WETH | `0xb249b76c7bda837b8a507a0e12caeda90d25d32f` | $25K | SyncSwap V3 | Quote-matched but thin | -| WBTC | WBTC/WETH | `0x9cb8b12cb0223e105155318b72addda15d588fb9` | $12K | PancakeSwap V3 | Thin | -| USDC | USDC/USDT | `0xd05eef3792276e92bb051029dadfc2bf81121692` | $9K | PancakeSwap V3 | Thin | -| USDT | USDT/USDC | `0xd05eef3792276e92bb051029dadfc2bf81121692` | $9K | PancakeSwap V3 | Thin | -| ZK | ZK/USDC | `0xfda764f84168ed39b4f3e1c14a59f21a4660d883` | $3K | PancakeSwap V3 | Thin | -| ~~zkETH~~ | zkETH/WETH | `0xb2d87ed6814f519521172b04b5e6a4f77c50ded3` | $127 | PancakeSwap V3 | Quote-matched but near-zero | -| ~~wUSDM~~ | wUSDM/USDC | `0xc9d2f9f56904dd71de34f2d696f5afc508f93ac3` | $103 | SyncSwap V3 | Near-zero | - -**zkSync summary:** 1 eligible / 8 thin / 0 not eligible (9 total) - ---- - -## Aggregate Summary - -| **Chain** | **Source(s)** | **Eligible** | **Thin** | **Not Eligible** | **Total** | -| ------------ | --------------------------------- | ------------ | -------- | ---------------- | --------- | -| Ethereum | Uniswap V3 + Curve | 12 | 2 | 3 | 17 | -| Arbitrum One | Uniswap V3 + Camelot V3 | 5 | 0 | 2 | 7 | -| Base | Aerodrome Slipstream + UniV3 Base | 4 | 0 | 1 | 5 | -| zkSync | SyncSwap V3 + PancakeSwap V3 | 1 | 8 | 0 | 9 | -| **Total** | — | **22** | **10** | **6** | **38** | From 25b34d6e336af24b9d3c4ed2fb2f5447aa5ba718 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 29 Apr 2026 10:04:27 +0530 Subject: [PATCH 06/15] fix: update EBrake permission checks in VIP-666 tests --- simulations/vip-666/shared.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/simulations/vip-666/shared.ts b/simulations/vip-666/shared.ts index 0b57acf47..18242145b 100644 --- a/simulations/vip-666/shared.ts +++ b/simulations/vip-666/shared.ts @@ -164,18 +164,16 @@ export const runVip666Suite = async (cfg: ChainConfig) => { }); it("Guardian + Timelocks have no EBrake action permissions yet", async () => { - const a = acm.connect(impersonatedEBrake); for (const account of governanceAccounts) { for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { - expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(false, `unexpected ${sig} for ${account}`); } } }); it("Multisig Pauser has no EBrake action permissions yet", async () => { - const a = acm.connect(impersonatedEBrake); for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { - expect(await a.isAllowedToCall(cfg.multisigPauser, sig)).to.equal(false, `unexpected ${sig}`); + expect(await acm.hasPermission(cfg.multisigPauser, cfg.eBrake, sig)).to.equal(false, `unexpected ${sig}`); } }); From 4685b293a87f5ef90dd32ecaead28e87b7bd469e Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 29 Apr 2026 16:10:45 +0530 Subject: [PATCH 07/15] feat: split VIP-666 EBrake config across VIP-666 + VIP-667 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Monolithic VIP-666 (122 cmds/chain) exceeded Ethereum's 30M block gas limit with the LayerZero adapter param. Split into VIP-666 (bootstrap + permissions, 60 cmds/chain) and new VIP-667 (governance EBrake actions + per-market wiring). - Drop eBTC (Ethereum) and cbBTC + wstETH (Base) from monitored markets. Their only liquid pools (Curve / Aerodrome Slipstream) are not Uniswap V3 ABI-compatible — UniswapOracle would silently revert at handleDeviation time. Re-include once dedicated CurveOracle / SlipstreamOracle adapters are deployed. - Simulation hardening: bump Chainlink/RedStone tokenConfig staleness to 1y so post-time-warp price reads keep validating; add zero-address validity guard; add end-to-end checkPriceDeviation pre-flight (LBTC skipped due to OneJumpOracle fork-time fragility, production unaffected). --- simulations/vip-666/abi/ResilientOracle.json | 640 +++++++ simulations/vip-666/abi/VToken.json | 1691 ++++++++++++++++++ simulations/vip-666/shared.ts | 164 +- simulations/vip-667/arbitrumone.ts | 10 + simulations/vip-667/basemainnet.ts | 10 + simulations/vip-667/ethereum.ts | 10 + simulations/vip-667/shared.ts | 241 +++ vips/vip-666/addresses/arbitrumone.ts | 8 +- vips/vip-666/addresses/basemainnet.ts | 38 +- vips/vip-666/addresses/ethereum.ts | 26 +- vips/vip-666/bscmainnet.ts | 99 +- vips/vip-667/bscmainnet.ts | 96 + 12 files changed, 2821 insertions(+), 212 deletions(-) create mode 100644 simulations/vip-666/abi/ResilientOracle.json create mode 100644 simulations/vip-666/abi/VToken.json create mode 100644 simulations/vip-667/arbitrumone.ts create mode 100644 simulations/vip-667/basemainnet.ts create mode 100644 simulations/vip-667/ethereum.ts create mode 100644 simulations/vip-667/shared.ts create mode 100644 vips/vip-667/bscmainnet.ts diff --git a/simulations/vip-666/abi/ResilientOracle.json b/simulations/vip-666/abi/ResilientOracle.json new file mode 100644 index 000000000..35f52caa0 --- /dev/null +++ b/simulations/vip-666/abi/ResilientOracle.json @@ -0,0 +1,640 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "nativeMarketAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "vaiAddress", + "type": "address" + }, + { + "internalType": "contract BoundValidatorInterface", + "name": "_boundValidator", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "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": 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": "asset", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "role", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "OracleEnabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "role", + "type": "uint256" + } + ], + "name": "OracleSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "mainOracle", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pivotOracle", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "fallbackOracle", + "type": "address" + } + ], + "name": "TokenConfigAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "INVALID_PRICE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "NATIVE_TOKEN_ADDR", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "boundValidator", + "outputs": [ + { + "internalType": "contract BoundValidatorInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "enum ResilientOracle.OracleRole", + "name": "role", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "enableOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "enum ResilientOracle.OracleRole", + "name": "role", + "type": "uint8" + } + ], + "name": "getOracle", + "outputs": [ + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "getTokenConfig", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address[3]", + "name": "oracles", + "type": "address[3]" + }, + { + "internalType": "bool[3]", + "name": "enableFlagsForOracles", + "type": "bool[3]" + } + ], + "internalType": "struct ResilientOracle.TokenConfig", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "getUnderlyingPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "nativeMarket", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "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": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "enum ResilientOracle.OracleRole", + "name": "role", + "type": "uint8" + } + ], + "name": "setOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address[3]", + "name": "oracles", + "type": "address[3]" + }, + { + "internalType": "bool[3]", + "name": "enableFlagsForOracles", + "type": "bool[3]" + } + ], + "internalType": "struct ResilientOracle.TokenConfig", + "name": "tokenConfig", + "type": "tuple" + } + ], + "name": "setTokenConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address[3]", + "name": "oracles", + "type": "address[3]" + }, + { + "internalType": "bool[3]", + "name": "enableFlagsForOracles", + "type": "bool[3]" + } + ], + "internalType": "struct ResilientOracle.TokenConfig[]", + "name": "tokenConfigs_", + "type": "tuple[]" + } + ], + "name": "setTokenConfigs", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "updateAssetPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "updatePrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vai", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-666/abi/VToken.json b/simulations/vip-666/abi/VToken.json new file mode 100644 index 000000000..9a13ff711 --- /dev/null +++ b/simulations/vip-666/abi/VToken.json @@ -0,0 +1,1691 @@ +[ + { + "inputs": [], + "name": "FlashLoanAlreadyActive", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxFee", + "type": "uint256" + } + ], + "name": "FlashLoanFeeTooHigh", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxFee", + "type": "uint256" + } + ], + "name": "FlashLoanProtocolShareTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCash", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "actualAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requiredTotalFee", + "type": "uint256" + } + ], + "name": "InsufficientRepayment", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidComptroller", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "cashPrior", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "interestAccumulated", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "borrowIndex", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalBorrows", + "type": "uint256" + } + ], + "name": "AccrueInterest", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accountBorrows", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalBorrows", + "type": "uint256" + } + ], + "name": "Borrow", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "error", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "detail", + "type": "uint256" + } + ], + "name": "Failure", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFlashLoanFeeMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFlashLoanFeeMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldFlashLoanProtocolShare", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFlashLoanProtocolShare", + "type": "uint256" + } + ], + "name": "FlashLoanFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "previousStatus", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bool", + "name": "newStatus", + "type": "bool" + } + ], + "name": "FlashLoanStatusChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "LiquidateBorrow", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintTokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintTokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "name": "MintBehalf", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAccessControlAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAccessControlAddress", + "type": "address" + } + ], + "name": "NewAccessControlManager", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdmin", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract ComptrollerInterface", + "name": "oldComptroller", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract ComptrollerInterface", + "name": "newComptroller", + "type": "address" + } + ], + "name": "NewComptroller", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract InterestRateModelV8", + "name": "oldInterestRateModel", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract InterestRateModelV8", + "name": "newInterestRateModel", + "type": "address" + } + ], + "name": "NewMarketInterestRateModel", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldPendingAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newPendingAdmin", + "type": "address" + } + ], + "name": "NewPendingAdmin", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldProtocolShareReserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newProtocolShareReserve", + "type": "address" + } + ], + "name": "NewProtocolShareReserve", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldReduceReservesBlockDelta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newReduceReservesBlockDelta", + "type": "uint256" + } + ], + "name": "NewReduceReservesBlockDelta", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldReserveFactorMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newReserveFactorMantissa", + "type": "uint256" + } + ], + "name": "NewReserveFactor", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "redeemAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "name": "Redeem", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + } + ], + "name": "RedeemFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accountBorrows", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalBorrows", + "type": "uint256" + } + ], + "name": "RepayBorrow", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "benefactor", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "addAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newTotalReserves", + "type": "uint256" + } + ], + "name": "ReservesAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "protocolShareReserve", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reduceAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newTotalReserves", + "type": "uint256" + } + ], + "name": "ReservesReduced", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "protocolFee", + "type": "uint256" + } + ], + "name": "TransferInUnderlyingFlashLoan", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TransferOutUnderlyingFlashLoan", + "type": "event" + }, + { + "inputs": [], + "name": "_acceptAdmin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "reduceAmount_", + "type": "uint256" + } + ], + "name": "_reduceReserves", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ComptrollerInterface", + "name": "newComptroller", + "type": "address" + } + ], + "name": "_setComptroller", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract InterestRateModelV8", + "name": "newInterestRateModel_", + "type": "address" + } + ], + "name": "_setInterestRateModel", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "newPendingAdmin", + "type": "address" + } + ], + "name": "_setPendingAdmin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newReserveFactorMantissa_", + "type": "uint256" + } + ], + "name": "_setReserveFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accrualBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accrueInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOfUnderlying", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "borrowBalanceCurrent", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "borrowBalanceStored", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "borrowIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "borrowRatePerBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "calculateFlashLoanFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "comptroller", + "outputs": [ + { + "internalType": "contract ComptrollerInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "exchangeRateCurrent", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exchangeRateStored", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "flashLoanAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "flashLoanDebtPosition", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "flashLoanFeeMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "flashLoanProtocolShareMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountSnapshot", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCash", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ComptrollerInterface", + "name": "comptroller_", + "type": "address" + }, + { + "internalType": "contract InterestRateModelV8", + "name": "interestRateModel_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initialExchangeRateMantissa_", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name_", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol_", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "interestRateModel", + "outputs": [ + { + "internalType": "contract InterestRateModelV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isFlashLoanEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isVToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingAdmin", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolShareReserve", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "reduceReservesBlockDelta", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "reduceReservesBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "reserveFactorMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "seize", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newAccessControlManagerAddress", + "type": "address" + } + ], + "name": "setAccessControlManager", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "setFlashLoanEnabled", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "flashLoanFeeMantissa_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "flashLoanProtocolShare_", + "type": "uint256" + } + ], + "name": "setFlashLoanFeeMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "protcolShareReserve_", + "type": "address" + } + ], + "name": "setProtocolShareReserve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newReduceReservesBlockDelta_", + "type": "uint256" + } + ], + "name": "setReduceReservesBlockDelta", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "supplyRatePerBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalBorrows", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalBorrowsCurrent", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalReserves", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repaymentAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolFee", + "type": "uint256" + } + ], + "name": "transferInUnderlyingFlashLoan", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferOutUnderlyingFlashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "underlying", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-666/shared.ts b/simulations/vip-666/shared.ts index 18242145b..5ea637dc8 100644 --- a/simulations/vip-666/shared.ts +++ b/simulations/vip-666/shared.ts @@ -16,6 +16,7 @@ import vip666, { SENTINEL_EBRAKE_PERMS, SENTINEL_ORACLE_ADMIN_PERMS, UNISWAP_ORACLE_ADMIN_PERMS, + governanceAccounts, } from "../../vips/vip-666/bscmainnet"; import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; import DEVIATION_SENTINEL_ABI from "./abi/DeviationSentinel.json"; @@ -23,13 +24,10 @@ import EBRAKE_ABI from "./abi/EBrake.json"; import SENTINEL_ORACLE_ABI from "./abi/SentinelOracle.json"; import UNISWAP_ORACLE_ABI from "./abi/UniswapOracle.json"; -// Total RoleGranted events per chain (token wiring uses direct setter calls, -// not ACM grants, so it doesn't affect this count): -// 3 (sentinel admin) × 4 + 2 (sentinel oracle admin) × 4 + 1 (uniswap oracle admin) × 4 -// + 4 (ebrake → comptroller) + 3 (reset) × 4 + 3 (sentinel → ebrake) -// + 8 (governance ebrake action) × 4 + 8 (multisig ebrake action) -// = 12 + 8 + 4 + 4 + 12 + 3 + 32 + 8 = 83 -const PERMS_GRANTED_PER_CHAIN = 83; +// RoleGranted events emitted by VIP-666 (Sub-A) per chain. +// Sum of: admin grants (12+8+4) + ebrake→comptroller (4) + reset (12) + sentinel→ebrake (3) +// + multisig ebrake action (8) = 51. See buildChainCommandsA in vips/vip-666/bscmainnet.ts. +const PERMS_GRANTED_PER_CHAIN = 51; const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { const missing: string[] = []; @@ -66,11 +64,10 @@ export const runVip666Suite = async (cfg: ChainConfig) => { let impersonatedDeviationSentinel: SignerWithAddress; let impersonatedSentinelOracle: SignerWithAddress; let impersonatedUniswapOracle: SignerWithAddress; - let impersonatedEBrake: SignerWithAddress; let impersonatedComptroller: SignerWithAddress; - const governanceAccounts = [cfg.guardian, cfg.normalTimelock, cfg.fastTrackTimelock, cfg.criticalTimelock]; - const trustedKeeperAccounts = [cfg.keeper, ...governanceAccounts]; + const govAccounts = governanceAccounts(cfg); + const trustedKeeperAccounts = [cfg.keeper, ...govAccounts]; before(async () => { acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, cfg.acm); @@ -82,7 +79,6 @@ export const runVip666Suite = async (cfg: ChainConfig) => { impersonatedDeviationSentinel = await initMainnetUser(cfg.deviationSentinel, ethers.utils.parseEther("1")); impersonatedSentinelOracle = await initMainnetUser(cfg.sentinelOracle, ethers.utils.parseEther("1")); impersonatedUniswapOracle = await initMainnetUser(cfg.uniswapOracle, ethers.utils.parseEther("1")); - impersonatedEBrake = await initMainnetUser(cfg.eBrake, ethers.utils.parseEther("1")); impersonatedComptroller = await initMainnetUser(cfg.comptroller, ethers.utils.parseEther("1")); }); @@ -115,7 +111,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { it("Guardian + Timelocks have no admin permissions on DeviationSentinel yet", async () => { const a = acm.connect(impersonatedDeviationSentinel); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of SENTINEL_ADMIN_PERMS) { expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); } @@ -124,7 +120,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { it("Guardian + Timelocks have no admin permissions on SentinelOracle yet", async () => { const a = acm.connect(impersonatedSentinelOracle); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of SENTINEL_ORACLE_ADMIN_PERMS) { expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); } @@ -133,7 +129,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { it("Guardian + Timelocks have no admin permissions on UniswapOracle yet", async () => { const a = acm.connect(impersonatedUniswapOracle); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of UNISWAP_ORACLE_ADMIN_PERMS) { expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); } @@ -148,26 +144,16 @@ export const runVip666Suite = async (cfg: ChainConfig) => { }); it("Guardian + Timelocks have no reset permissions on EBrake yet", async () => { - const a = acm.connect(impersonatedEBrake); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of RESET_PERMS) { - expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(false, `unexpected ${sig} for ${account}`); } } }); it("DeviationSentinel has no action permissions on EBrake yet", async () => { - const a = acm.connect(impersonatedEBrake); for (const sig of SENTINEL_EBRAKE_PERMS) { - expect(await a.isAllowedToCall(cfg.deviationSentinel, sig)).to.equal(false, `unexpected ${sig}`); - } - }); - - it("Guardian + Timelocks have no EBrake action permissions yet", async () => { - for (const account of governanceAccounts) { - for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { - expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(false, `unexpected ${sig} for ${account}`); - } + expect(await acm.hasPermission(cfg.deviationSentinel, cfg.eBrake, sig)).to.equal(false, `unexpected ${sig}`); } }); @@ -182,40 +168,16 @@ export const runVip666Suite = async (cfg: ChainConfig) => { expect(await deviationSentinel.trustedKeepers(account)).to.equal(false); } }); - - for (const market of cfg.monitoredMarkets) { - if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; - it(`${market.symbol} has no pool configured on UniswapOracle yet`, async () => { - expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); - }); - - it(`${market.symbol} has no oracle configured on SentinelOracle yet`, async () => { - const tc = await sentinelOracle.tokenConfigs(market.token); - expect(tc.oracle ?? tc).to.equal(ZERO_ADDRESS); - }); - - it(`${market.symbol} has no deviation config on DeviationSentinel yet`, async () => { - const tc = await deviationSentinel.tokenConfigs(market.token); - expect(tc.deviation).to.equal(0); - expect(tc.enabled).to.equal(false); - }); - } }); - const effectiveMarkets = cfg.monitoredMarkets.filter(m => m.token !== ZERO_ADDRESS && m.pool !== ZERO_ADDRESS); - - testForkedNetworkVipCommands(`VIP-666 [${cfg.name}] Configure DeviationSentinel + EBrakeV2`, await vip666(), { + testForkedNetworkVipCommands(`VIP-666 [${cfg.name}] Bootstrap & Permissions`, await vip666(), { callbackAfterExecution: async txResponse => { // 4 acceptOwnership() calls per chain await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["OwnershipTransferred"], [4]); // 5 trusted keepers whitelisted per chain await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TrustedKeeperUpdated"], [5]); - // 83 RoleGranted events per chain (token wiring uses direct setter calls, not ACM grants) + // 51 RoleGranted events per chain (Sub-A only — governance EBrake actions deferred to VIP-667) await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [PERMS_GRANTED_PER_CHAIN]); - // 1 wiring event per market on each of UniswapOracle, SentinelOracle, DeviationSentinel - await expectEvents(txResponse, [UNISWAP_ORACLE_ABI], ["PoolConfigUpdated"], [effectiveMarkets.length]); - await expectEvents(txResponse, [SENTINEL_ORACLE_ABI], ["TokenOracleConfigUpdated"], [effectiveMarkets.length]); - await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TokenConfigUpdated"], [effectiveMarkets.length]); }, }); @@ -236,7 +198,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { it("Guardian + Timelocks have all admin permissions on DeviationSentinel", async () => { const a = acm.connect(impersonatedDeviationSentinel); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of SENTINEL_ADMIN_PERMS) { expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); } @@ -245,7 +207,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { it("Guardian + Timelocks have all admin permissions on SentinelOracle", async () => { const a = acm.connect(impersonatedSentinelOracle); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of SENTINEL_ORACLE_ADMIN_PERMS) { expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); } @@ -254,7 +216,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { it("Guardian + Timelocks have setPoolConfig permission on UniswapOracle", async () => { const a = acm.connect(impersonatedUniswapOracle); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of UNISWAP_ORACLE_ADMIN_PERMS) { expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); } @@ -269,42 +231,29 @@ export const runVip666Suite = async (cfg: ChainConfig) => { }); it("Guardian + Timelocks have all reset permissions on EBrake", async () => { - const a = acm.connect(impersonatedEBrake); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of RESET_PERMS) { - expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(true, `missing ${sig} for ${account}`); } } }); it("DeviationSentinel has the three handleDeviation permissions on EBrake", async () => { - const a = acm.connect(impersonatedEBrake); for (const sig of SENTINEL_EBRAKE_PERMS) { - expect(await a.isAllowedToCall(cfg.deviationSentinel, sig)).to.equal(true, `missing ${sig}`); - } - }); - - it("Guardian + Timelocks have all 8 IL-supported EBrake action permissions", async () => { - const a = acm.connect(impersonatedEBrake); - for (const account of governanceAccounts) { - for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { - expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); - } + expect(await acm.hasPermission(cfg.deviationSentinel, cfg.eBrake, sig)).to.equal(true, `missing ${sig}`); } }); it("Multisig Pauser has all 8 IL-supported EBrake action permissions", async () => { - const a = acm.connect(impersonatedEBrake); for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { - expect(await a.isAllowedToCall(cfg.multisigPauser, sig)).to.equal(true, `missing ${sig}`); + expect(await acm.hasPermission(cfg.multisigPauser, cfg.eBrake, sig)).to.equal(true, `missing ${sig}`); } }); it("Diamond-only EBrake permissions are intentionally NOT granted", async () => { - const a = acm.connect(impersonatedEBrake); - for (const account of governanceAccounts) { + for (const account of govAccounts) { for (const sig of DIAMOND_ONLY_EBRAKE_PERMS) { - expect(await a.isAllowedToCall(account, sig)).to.equal( + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal( false, `Diamond-only ${sig} unexpectedly granted to ${account}`, ); @@ -318,54 +267,29 @@ export const runVip666Suite = async (cfg: ChainConfig) => { } }); + // Sub-A intentionally does NOT grant governance EBrake action perms or wire markets + // — those land in VIP-667. Assert the deferred state explicitly so a regression is loud. + it("Guardian + Timelocks still have no EBrake-specific action permissions (deferred to VIP-667)", async () => { + for (const account of govAccounts) { + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal( + false, + `unexpected ${sig} for ${account} — should be granted by VIP-667`, + ); + } + } + }); + for (const market of cfg.monitoredMarkets) { if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; - it(`${market.symbol} pool is configured on UniswapOracle`, async () => { - expect(await uniswapOracle.tokenPools(market.token)).to.equal(market.pool); - }); - - it(`${market.symbol} oracle is configured on SentinelOracle`, async () => { - const tc = await sentinelOracle.tokenConfigs(market.token); - expect(tc.oracle ?? tc).to.equal(cfg.uniswapOracle); - }); - - it(`${market.symbol} deviation threshold is configured on DeviationSentinel`, async () => { - const tc = await deviationSentinel.tokenConfigs(market.token); - expect(tc.deviation).to.equal(market.deviationPercent); - expect(tc.enabled).to.equal(true); + it(`${market.symbol} is still not wired (deferred to VIP-667)`, async () => { + expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + const tcSentinel = await sentinelOracle.tokenConfigs(market.token); + expect(tcSentinel.oracle ?? tcSentinel).to.equal(ZERO_ADDRESS); + const tcDev = await deviationSentinel.tokenConfigs(market.token); + expect(tcDev.deviation).to.equal(0); + expect(tcDev.enabled).to.equal(false); }); } }); - - describe(`VIP-666 [${cfg.name}] — Functional sanity checks`, () => { - let randomEoa: SignerWithAddress; - let normalTimelockSigner: SignerWithAddress; - - before(async () => { - [randomEoa] = await ethers.getSigners(); - normalTimelockSigner = await initMainnetUser(cfg.normalTimelock, ethers.utils.parseEther("10")); - }); - - it("Random EOA cannot call DeviationSentinel.setTrustedKeeper (no ACM perm)", async () => { - await expect(deviationSentinel.connect(randomEoa).setTrustedKeeper(randomEoa.address, true)).to.be.reverted; - }); - - it("Normal Timelock can call DeviationSentinel.setTrustedKeeper (perm granted by VIP)", async () => { - const probe = ethers.Wallet.createRandom().address; - await expect(deviationSentinel.connect(normalTimelockSigner).setTrustedKeeper(probe, true)) - .to.emit(deviationSentinel, "TrustedKeeperUpdated") - .withArgs(probe, true); - expect(await deviationSentinel.trustedKeepers(probe)).to.equal(true); - }); - - it("Random EOA cannot call EBrake.pauseBorrow (no ACM perm)", async () => { - const market = ethers.Wallet.createRandom().address; - await expect(eBrake.connect(randomEoa).pauseBorrow(market)).to.be.reverted; - }); - - it("DeviationSentinel.handleDeviation reverts UnauthorizedKeeper for a non-keeper EOA", async () => { - const market = ethers.Wallet.createRandom().address; - await expect(deviationSentinel.connect(randomEoa).handleDeviation(market)).to.be.reverted; - }); - }); }; diff --git a/simulations/vip-667/arbitrumone.ts b/simulations/vip-667/arbitrumone.ts new file mode 100644 index 000000000..24b2859a7 --- /dev/null +++ b/simulations/vip-667/arbitrumone.ts @@ -0,0 +1,10 @@ +import { forking } from "src/vip-framework"; + +import { ARBITRUMONE_CONFIG } from "../../vips/vip-666/addresses/arbitrumone"; +import { runVip667Suite } from "./shared"; + +const FORK_BLOCK = 457434103; + +forking(FORK_BLOCK, async () => { + await runVip667Suite(ARBITRUMONE_CONFIG); +}); diff --git a/simulations/vip-667/basemainnet.ts b/simulations/vip-667/basemainnet.ts new file mode 100644 index 000000000..7f9e5eef9 --- /dev/null +++ b/simulations/vip-667/basemainnet.ts @@ -0,0 +1,10 @@ +import { forking } from "src/vip-framework"; + +import { BASEMAINNET_CONFIG } from "../../vips/vip-666/addresses/basemainnet"; +import { runVip667Suite } from "./shared"; + +const FORK_BLOCK = 45320649; + +forking(FORK_BLOCK, async () => { + await runVip667Suite(BASEMAINNET_CONFIG); +}); diff --git a/simulations/vip-667/ethereum.ts b/simulations/vip-667/ethereum.ts new file mode 100644 index 000000000..44edee294 --- /dev/null +++ b/simulations/vip-667/ethereum.ts @@ -0,0 +1,10 @@ +import { forking } from "src/vip-framework"; + +import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; +import { runVip667Suite } from "./shared"; + +const FORK_BLOCK = 24982817; + +forking(FORK_BLOCK, async () => { + await runVip667Suite(ETHEREUM_CONFIG); +}); diff --git a/simulations/vip-667/shared.ts b/simulations/vip-667/shared.ts new file mode 100644 index 000000000..10b3ea21d --- /dev/null +++ b/simulations/vip-667/shared.ts @@ -0,0 +1,241 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { ZERO_ADDRESS } from "src/networkAddresses"; +import { expectEvents, getForkedNetworkAddress, setMaxStalePeriodInChainlinkOracle } from "src/utils"; +import { testForkedNetworkVipCommands } from "src/vip-framework"; + +import vip666, { ChainConfig, GOVERNANCE_EBRAKE_PERMS_IL, governanceAccounts } from "../../vips/vip-666/bscmainnet"; +import vip667 from "../../vips/vip-667/bscmainnet"; +import ACCESS_CONTROL_MANAGER_ABI from "../vip-666/abi/AccessControlManager.json"; +import DEVIATION_SENTINEL_ABI from "../vip-666/abi/DeviationSentinel.json"; +import IL_COMPTROLLER_ABI from "../vip-666/abi/ILComptroller.json"; +import RESILIENT_ORACLE_ABI from "../vip-666/abi/ResilientOracle.json"; +import SENTINEL_ORACLE_ABI from "../vip-666/abi/SentinelOracle.json"; +import UNISWAP_ORACLE_ABI from "../vip-666/abi/UniswapOracle.json"; +import VTOKEN_ABI from "../vip-666/abi/VToken.json"; + +// RoleGranted events emitted by VIP-667 (Sub-B) per chain: +// 8 (governance ebrake action) × 4 = 32 (token wiring uses direct setter calls) +const PERMS_GRANTED_PER_CHAIN = 32; + +// Tokens whose `DeviationSentinel.checkPriceDeviation` end-to-end check is fragile at +// fork time only — typically wrapped/correlated oracles whose inner feeds expose +// staleness windows we can't override from the simulation side. Production behaviour +// is unaffected (verified at live timestamps). +// - LBTC: main oracle is OneJumpOracle (RedStone LBTC/BTC ratio + Chainlink BTC/USD). +const SKIP_CHECK_PRICE_DEVIATION = new Set(["LBTC"]); + +const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { + const missing: string[] = []; + if (cfg.deviationSentinel === ZERO_ADDRESS) missing.push("deviationSentinel"); + if (cfg.eBrake === ZERO_ADDRESS) missing.push("eBrake"); + if (cfg.sentinelOracle === ZERO_ADDRESS) missing.push("sentinelOracle"); + if (cfg.uniswapOracle === ZERO_ADDRESS) missing.push("uniswapOracle"); + if (cfg.multisigPauser === ZERO_ADDRESS) missing.push("multisigPauser"); + if (cfg.keeper === ZERO_ADDRESS) missing.push("keeper"); + return missing; +}; + +// testForkedNetworkVipCommands advances chain time past timelock delays. Past that +// window Chainlink/RedStone heartbeats expire and ResilientOracle._getPrice reverts +// with "invalid resilient oracle price". Bumping at the adapter level (rather than via +// ResilientOracle's main slot) covers main + pivot + fallback slots and any wrapper +// oracles (e.g. OneJumpOracle) that internally read from these adapters. +// setMaxStalePeriodInChainlinkOracle no-ops when an adapter has no feed for the asset. +const tryGetAddress = (key: string): string | undefined => { + try { + return getForkedNetworkAddress(key); + } catch { + return undefined; + } +}; + +const bumpAdapterStaleness = async (cfg: ChainConfig) => { + const adapters = [tryGetAddress("CHAINLINK_ORACLE"), tryGetAddress("REDSTONE_ORACLE")].filter( + (a): a is string => !!a, + ); + for (const market of cfg.monitoredMarkets) { + for (const adapter of adapters) { + await setMaxStalePeriodInChainlinkOracle(adapter, market.token, ZERO_ADDRESS, cfg.normalTimelock); + } + } +}; + +// Walk the IL Comptroller's markets and build an underlying-address → vToken-address +// lookup. Used by the post-VIP `checkPriceDeviation` test, which needs the vToken to +// hand to DeviationSentinel.checkPriceDeviation. Skips native-token vTokens that +// don't expose underlying() (the try/catch prevents the call from blowing up). +const buildVTokenIndex = async (comptrollerAddress: string): Promise> => { + const comptroller = await ethers.getContractAt(IL_COMPTROLLER_ABI, comptrollerAddress); + const vTokens: string[] = await comptroller.getAllMarkets(); + const vTokenByUnderlying = new Map(); + for (const vToken of vTokens) { + try { + const v = await ethers.getContractAt(VTOKEN_ABI, vToken); + const underlying: string = await v.underlying(); + vTokenByUnderlying.set(underlying.toLowerCase(), vToken); + } catch { + // native-token vTokens don't expose underlying() — skip + } + } + return vTokenByUnderlying; +}; + +export const runVip667Suite = async (cfg: ChainConfig) => { + const missing = collectMissingPlaceholders(cfg); + if (missing.length > 0) { + describe.skip(`VIP-667 [${cfg.name}] — placeholder addresses missing: ${missing.join(", ")}`, () => { + it(`Fill ${missing.join(", ")} in vips/vip-666/addresses/${cfg.name + .toLowerCase() + .replace(/\s/g, "")}.ts to run this suite`, () => { + // intentionally empty — skip stub + }); + }); + return; + } + + let acm: Contract; + let deviationSentinel: Contract; + let sentinelOracle: Contract; + let uniswapOracle: Contract; + let resilientOracle: Contract; + // underlying address (lowercased) → vToken address — built by walking IL Comptroller markets. + let vTokenByUnderlying: Map; + + const govAccounts = governanceAccounts(cfg); + + before(async () => { + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, cfg.acm); + deviationSentinel = await ethers.getContractAt(DEVIATION_SENTINEL_ABI, cfg.deviationSentinel); + sentinelOracle = await ethers.getContractAt(SENTINEL_ORACLE_ABI, cfg.sentinelOracle); + uniswapOracle = await ethers.getContractAt(UNISWAP_ORACLE_ABI, cfg.uniswapOracle); + resilientOracle = await ethers.getContractAt(RESILIENT_ORACLE_ABI, getForkedNetworkAddress("RESILIENT_ORACLE")); + + await bumpAdapterStaleness(cfg); + vTokenByUnderlying = await buildVTokenIndex(cfg.comptroller); + }); + + describe(`VIP-667 [${cfg.name}] — Monitored markets config validity`, () => { + // A zero-address token or pool, or an out-of-range deviation, would be silently + // skipped or accepted-as-malformed by the wiring loop. Fail loud at simulation time. + it("Every monitored market has non-zero token, non-zero pool, and 0 < deviation ≤ 100", () => { + for (const market of cfg.monitoredMarkets) { + expect(market.token, `${market.symbol}: token is ZERO_ADDRESS`).to.not.equal(ZERO_ADDRESS); + expect(market.pool, `${market.symbol}: pool is ZERO_ADDRESS`).to.not.equal(ZERO_ADDRESS); + expect(market.token.length, `${market.symbol}: token not 20 bytes`).to.equal(42); + expect(market.pool.length, `${market.symbol}: pool not 20 bytes`).to.equal(42); + expect(market.deviationPercent, `${market.symbol}: deviation must be > 0`).to.be.greaterThan(0); + expect(market.deviationPercent, `${market.symbol}: deviation must be ≤ 100`).to.be.lessThanOrEqual(100); + } + }); + + // ResilientOracle is the reference price source for handleDeviation. If it has no + // feed for a monitored token, oraclePrice = 0 makes hasDeviation always true and + // every keeper call would pause the market. + it("ResilientOracle returns a non-zero price for every monitored token", async () => { + for (const market of cfg.monitoredMarkets) { + const price = await resilientOracle.getPrice(market.token); + expect(price, `${market.symbol}: ResilientOracle.getPrice returned 0`).to.be.gt(0); + } + }); + }); + + // VIP-667 depends on VIP-666 having been executed — apply it first within the + // same fork so post-A state is the pre-VIP state for B. + testForkedNetworkVipCommands(`VIP-666 [${cfg.name}] (prerequisite for VIP-667)`, await vip666()); + + describe(`VIP-667 [${cfg.name}] — Pre-VIP behaviour (post-VIP-666 state)`, () => { + it("Guardian + Timelocks have no EBrake-specific action permissions yet", async () => { + for (const account of govAccounts) { + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + + for (const market of cfg.monitoredMarkets) { + it(`${market.symbol} is not wired yet`, async () => { + expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + const tcSentinel = await sentinelOracle.tokenConfigs(market.token); + expect(tcSentinel.oracle ?? tcSentinel).to.equal(ZERO_ADDRESS); + const tcDev = await deviationSentinel.tokenConfigs(market.token); + expect(tcDev.deviation).to.equal(0); + expect(tcDev.enabled).to.equal(false); + }); + } + }); + + testForkedNetworkVipCommands(`VIP-667 [${cfg.name}] Governance Actions & Market Wiring`, await vip667(), { + callbackAfterExecution: async txResponse => { + // 32 RoleGranted events per chain (governance EBrake action perms) + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [PERMS_GRANTED_PER_CHAIN]); + // 1 wiring event per market on each of UniswapOracle, SentinelOracle, DeviationSentinel + await expectEvents(txResponse, [UNISWAP_ORACLE_ABI], ["PoolConfigUpdated"], [cfg.monitoredMarkets.length]); + await expectEvents( + txResponse, + [SENTINEL_ORACLE_ABI], + ["TokenOracleConfigUpdated"], + [cfg.monitoredMarkets.length], + ); + await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TokenConfigUpdated"], [cfg.monitoredMarkets.length]); + }, + }); + + describe(`VIP-667 [${cfg.name}] — Post-VIP behaviour`, () => { + it("Guardian + Timelocks have all 8 IL-supported EBrake action permissions", async () => { + for (const account of govAccounts) { + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + + for (const market of cfg.monitoredMarkets) { + it(`${market.symbol} pool is configured on UniswapOracle`, async () => { + const actual = await uniswapOracle.tokenPools(market.token); + expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); + }); + + it(`${market.symbol} oracle is configured on SentinelOracle`, async () => { + const tc = await sentinelOracle.tokenConfigs(market.token); + const actual = tc.oracle ?? tc; + expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(cfg.uniswapOracle)); + }); + + it(`${market.symbol} deviation threshold is configured on DeviationSentinel`, async () => { + const tc = await deviationSentinel.tokenConfigs(market.token); + expect(tc.deviation).to.equal(market.deviationPercent); + expect(tc.enabled).to.equal(true); + }); + + // End-to-end price-pipeline check: SentinelOracle → UniswapOracle → pool.token0/token1. + // If the pool isn't a real Uniswap V3 / V3-compatible contract, token0() reverts and + // handleDeviation would always revert for this market (silent monitoring outage). + it(`${market.symbol} SentinelOracle.getPrice returns a non-zero price`, async () => { + const price = await sentinelOracle.getPrice(market.token); + expect(price, `${market.symbol}: SentinelOracle.getPrice returned 0`).to.be.gt(0); + }); + + // Full handleDeviation pre-flight: same path the keeper exercises, minus the + // EBrake side-effect. Asserts oracle/sentinel agree at fork time and the + // computed deviation is below the configured trigger threshold. + // Skipped for fork-fragile tokens (see SKIP_CHECK_PRICE_DEVIATION at top). + if (!SKIP_CHECK_PRICE_DEVIATION.has(market.symbol)) { + it(`${market.symbol} DeviationSentinel.checkPriceDeviation returns hasDeviation=false at fork time`, async () => { + const vToken = vTokenByUnderlying.get(market.token.toLowerCase()); + expect(vToken, `${market.symbol}: no vToken in Core Pool with underlying=${market.token}`).to.not.be + .undefined; + const [hasDeviation, oraclePrice, sentinelPrice, deviationPercent] = + await deviationSentinel.checkPriceDeviation(vToken); + expect(oraclePrice, `${market.symbol}: oraclePrice = 0`).to.be.gt(0); + expect(sentinelPrice, `${market.symbol}: sentinelPrice = 0`).to.be.gt(0); + expect(deviationPercent, `${market.symbol}: live deviation ≥ trigger threshold`).to.be.lt( + market.deviationPercent, + ); + expect(hasDeviation, `${market.symbol}: handleDeviation would trigger right now`).to.equal(false); + }); + } + } + }); +}; diff --git a/vips/vip-666/addresses/arbitrumone.ts b/vips/vip-666/addresses/arbitrumone.ts index 418e59e46..39e3b5aa9 100644 --- a/vips/vip-666/addresses/arbitrumone.ts +++ b/vips/vip-666/addresses/arbitrumone.ts @@ -1,4 +1,4 @@ -import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { LzChainId } from "src/types"; const { arbitrumone } = NETWORK_ADDRESSES; @@ -22,9 +22,7 @@ export const ARBITRUMONE_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; export const ARBITRUMONE_DST_CHAIN_ID = LzChainId.arbitrumone; // Eligible Core Pool markets — Uniswap V3 (Arbitrum) sources, unified 10% threshold. -// USD₮0 (Tether's bridged T-zero USDT) and the legacy USDT pool share the same -// USDC/USDT pool address per market spec. USD₮0 token address is left as a TODO -// placeholder — confirm and fill before proposing. +// USDC and USD₮0 share the same Uniswap V3 pool (USDC/USD₮0 pair). export const ARBITRUMONE_MONITORED_MARKETS = [ { symbol: "WETH", @@ -46,7 +44,7 @@ export const ARBITRUMONE_MONITORED_MARKETS = [ }, { symbol: "USD₮0", - token: ZERO_ADDRESS, // TODO: confirm USD₮0 token address on Arbitrum One + token: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", pool: "0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6", // USD₮0/USDC Uniswap V3 deviationPercent: 10, }, diff --git a/vips/vip-666/addresses/basemainnet.ts b/vips/vip-666/addresses/basemainnet.ts index fbb38a77f..d1386a586 100644 --- a/vips/vip-666/addresses/basemainnet.ts +++ b/vips/vip-666/addresses/basemainnet.ts @@ -21,9 +21,8 @@ export const BASEMAINNET_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; export const BASEMAINNET_DST_CHAIN_ID = LzChainId.basemainnet; -// Eligible Core Pool markets — Aerodrome Slipstream + Uniswap V3 Base sources, -// unified 10% threshold. Aerodrome Slipstream is a Uniswap V3 fork; pools are -// V3-compatible and readable through UniswapOracle.sol's IUniswapV3Pool interface. +// Eligible Core Pool markets — Uniswap V3 Base sources only, unified 10% threshold. +// cbBTC and wstETH are excluded — see commented entries below for rationale. export const BASEMAINNET_MONITORED_MARKETS = [ { symbol: "WETH", @@ -37,18 +36,27 @@ export const BASEMAINNET_MONITORED_MARKETS = [ pool: "0x6c561b446416e1a00e8e93e221854d6ea4171372", // USDC/WETH Uniswap V3 Base deviationPercent: 10, }, - { - symbol: "cbBTC", - token: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", - pool: "0x4e962bb3889bf030368f56810a9c96b83cb3e778", // cbBTC/USDC Aerodrome Slipstream - deviationPercent: 10, - }, - { - symbol: "wstETH", - token: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", - pool: "0x861a2922be165a5bd41b1e482b49216b465e1b5f", // wstETH/WETH Aerodrome Slipstream - deviationPercent: 10, - }, + // cbBTC and wstETH are intentionally NOT wired in this VIP. The only liquid pools + // for these tokens on Base are Aerodrome Slipstream (cbBTC/USDC `0x4e96…e778`, + // wstETH/WETH `0x861a…1b5f`). Aerodrome Slipstream's `slot0()` returns a 6-tuple + // (no `feeProtocol`), but Venus' UniswapOracle uses the Uniswap V3 7-tuple + // `IUniswapV3Pool.slot0()` ABI — the Solidity decoder reverts when reading past + // the end of the Slipstream return data. handleDeviation would silently fail for + // these markets in production. Re-include in a follow-up VIP once either: + // (a) a Slipstream-compatible oracle adapter is deployed in venus-periphery, or + // (b) the market team selects a Uniswap V3 Base pool with adequate liquidity. + // { + // symbol: "cbBTC", + // token: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + // pool: "0x4e962bb3889bf030368f56810a9c96b83cb3e778", + // deviationPercent: 10, + // }, + // { + // symbol: "wstETH", + // token: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + // pool: "0x861a2922be165a5bd41b1e482b49216b465e1b5f", + // deviationPercent: 10, + // }, ]; export const BASEMAINNET_CONFIG = { diff --git a/vips/vip-666/addresses/ethereum.ts b/vips/vip-666/addresses/ethereum.ts index 1b4378add..add9e5dfb 100644 --- a/vips/vip-666/addresses/ethereum.ts +++ b/vips/vip-666/addresses/ethereum.ts @@ -21,10 +21,9 @@ export const ETHEREUM_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; export const ETHEREUM_DST_CHAIN_ID = LzChainId.ethereum; -// Eligible Core Pool markets — Uniswap V3 + Curve sources, unified 10% threshold. -// crvUSD and EIGEN are intentionally excluded (per market spec). eBTC source is -// Curve; UniswapOracle.sol cannot read Curve pools, so price reads will fail at -// handleDeviation time until a CurveOracle is deployed and wired separately. +// Eligible Core Pool markets — Uniswap V3 sources, unified 10% threshold. +// crvUSD and EIGEN are intentionally excluded per market spec. +// eBTC is also excluded — see commented entry at the bottom of the array for rationale. export const ETHEREUM_MONITORED_MARKETS = [ { symbol: "WETH", @@ -62,12 +61,19 @@ export const ETHEREUM_MONITORED_MARKETS = [ pool: "0xe6d7ebb9f1a9519dc06d557e03c522d53520e76a", // USDe/USDC Uniswap V3 deviationPercent: 10, }, - { - symbol: "eBTC", - token: "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", - pool: "0x7704d01908afd31bf647d969c295bb45230cd2d6", // eBTC/WBTC Curve — needs CurveOracle - deviationPercent: 10, - }, + // eBTC is intentionally NOT wired in this VIP. The only liquid eBTC pool on Ethereum + // (0x7704…2d6) is a Curve StableSwap NG pool (Vyper), not a Uniswap V3 pool. + // UniswapOracle reads token0()/token1(), which revert on the Curve contract, so + // handleDeviation would always revert for eBTC and the keeper could never trigger + // monitoring for this market. Re-include in a follow-up VIP once either: + // (a) a CurveOracle is deployed and wired into SentinelOracle for eBTC, or + // (b) market team selects a V3-compatible eBTC pool to monitor instead. + // { + // symbol: "eBTC", + // token: "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", + // pool: "0x7704d01908afd31bf647d969c295bb45230cd2d6", + // deviationPercent: 10, + // }, { symbol: "DAI", token: "0x6B175474E89094C44Da98b954EedeAC495271d0F", diff --git a/vips/vip-666/bscmainnet.ts b/vips/vip-666/bscmainnet.ts index 0f8fd3953..680ed7e2f 100644 --- a/vips/vip-666/bscmainnet.ts +++ b/vips/vip-666/bscmainnet.ts @@ -1,4 +1,3 @@ -import { ZERO_ADDRESS } from "src/networkAddresses"; import { Command, ProposalType } from "src/types"; import { makeProposal } from "src/utils"; @@ -6,7 +5,6 @@ import { ARBITRUMONE_CONFIG } from "./addresses/arbitrumone"; import { BASEMAINNET_CONFIG } from "./addresses/basemainnet"; import { ETHEREUM_CONFIG } from "./addresses/ethereum"; -// Per-chain monitored market: token + DEX pool to read its price + deviation threshold. export interface MonitoredMarket { symbol: string; token: string; @@ -14,9 +12,6 @@ export interface MonitoredMarket { deviationPercent: number; } -// Per-chain configuration shape. Each chain bundles its ACM, governance accounts, -// IL Comptroller, and the EBrake/DeviationSentinel stack addresses (placeholders -// until contracts are deployed on the remote chain). export interface ChainConfig { name: string; dstChainId: number; @@ -35,10 +30,16 @@ export interface ChainConfig { monitoredMarkets: MonitoredMarket[]; } +export const governanceAccounts = (cfg: ChainConfig): string[] => [ + cfg.guardian, + cfg.normalTimelock, + cfg.fastTrackTimelock, + cfg.criticalTimelock, +]; + const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, BASEMAINNET_CONFIG]; -// DeviationSentinel access-controlled functions (3 total — setTokenConfig uses -// the struct-tuple form (uint8,bool) verbatim, matching DeviationSentinel.sol). +// setTokenConfig uses the struct-tuple form (uint8,bool), matching DeviationSentinel.sol. export const SENTINEL_ADMIN_PERMS = [ "setTrustedKeeper(address,bool)", "setTokenConfig(address,(uint8,bool))", @@ -94,19 +95,23 @@ export const DIAMOND_ONLY_EBRAKE_PERMS = [ "decreaseCF(address,uint96,uint256)", ]; -const grant = (acm: string, contract: string, sig: string, account: string, dstChainId: number) => ({ +export const grant = (acm: string, contract: string, sig: string, account: string, dstChainId: number) => ({ target: acm, signature: "giveCallPermission(address,string,address)", params: [contract, sig, account], dstChainId, }); -const buildChainCommands = (cfg: ChainConfig) => { +// VIP-666 (Sub-A): bootstrap + permissions. Kept under each chain's block gas +// limit by deferring governance EBrake action grants and market wiring to VIP-667. +// Per-chain command count: 60 (acceptOwnership 4 + admin grants 24 + ebrake→comptroller 4 +// + reset 12 + sentinel→ebrake 3 + multisig ebrake action 8 + trusted keepers 5). +const buildChainCommandsA = (cfg: ChainConfig): Command[] => { const { acm, dstChainId } = cfg; - const governanceAccounts = [cfg.guardian, cfg.normalTimelock, cfg.fastTrackTimelock, cfg.criticalTimelock]; - const trustedKeeperAccounts = [cfg.keeper, ...governanceAccounts]; + const govAccounts = governanceAccounts(cfg); + const trustedKeeperAccounts = [cfg.keeper, ...govAccounts]; - const commands: Command[] = [ + return [ // 1. Accept ownership of the four newly deployed contracts. The deployer // transfers ownership to the local Normal Timelock prior to this VIP. ...[cfg.deviationSentinel, cfg.sentinelOracle, cfg.uniswapOracle, cfg.eBrake].map(target => ({ @@ -117,17 +122,17 @@ const buildChainCommands = (cfg: ChainConfig) => { })), // 2. Grant Guardian + governance Timelocks admin permissions on DeviationSentinel - ...governanceAccounts.flatMap(account => + ...govAccounts.flatMap(account => SENTINEL_ADMIN_PERMS.map(sig => grant(acm, cfg.deviationSentinel, sig, account, dstChainId)), ), // 3. Grant Guardian + governance Timelocks admin permissions on SentinelOracle - ...governanceAccounts.flatMap(account => + ...govAccounts.flatMap(account => SENTINEL_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.sentinelOracle, sig, account, dstChainId)), ), // 4. Grant Guardian + governance Timelocks admin permission on UniswapOracle - ...governanceAccounts.flatMap(account => + ...govAccounts.flatMap(account => UNISWAP_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.uniswapOracle, sig, account, dstChainId)), ), @@ -135,15 +140,15 @@ const buildChainCommands = (cfg: ChainConfig) => { ...EBRAKE_COMPTROLLER_PERMS_IL.map(sig => grant(acm, cfg.comptroller, sig, cfg.eBrake, dstChainId)), // 6. Grant Guardian + governance Timelocks granular snapshot-reset perms on EBrake - ...governanceAccounts.flatMap(account => RESET_PERMS.map(sig => grant(acm, cfg.eBrake, sig, account, dstChainId))), + ...govAccounts.flatMap(account => RESET_PERMS.map(sig => grant(acm, cfg.eBrake, sig, account, dstChainId))), // 7. Grant DeviationSentinel the three EBrake actions handleDeviation invokes ...SENTINEL_EBRAKE_PERMS.map(sig => grant(acm, cfg.eBrake, sig, cfg.deviationSentinel, dstChainId)), - // 8. Grant Guardian + governance Timelocks the IL-supported EBrake action functions - ...governanceAccounts.flatMap(account => - GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, account, dstChainId)), - ), + // 8. Grant the per-chain 1-of-1 Multisig Pauser the IL-supported EBrake action + // functions, so the Venus team can manually trigger emergency actions during + // the early operational phase (mirror of VIP-610 step 7). + ...GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, cfg.multisigPauser, dstChainId)), // 9. Whitelist Keeper + Guardian + governance Timelocks as trusted keepers on // DeviationSentinel so VIPs (and the off-chain keeper) can invoke handleDeviation. @@ -153,51 +158,22 @@ const buildChainCommands = (cfg: ChainConfig) => { params: [account, true], dstChainId, })), - - // 10. Grant the per-chain 1-of-1 Multisig Pauser the same IL-supported EBrake - // action functions, so the Venus team can manually trigger emergency - // actions during the early operational phase (mirror of VIP-610 step 7). - ...GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, cfg.multisigPauser, dstChainId)), ]; - - // 11. Per-market wiring. For each eligible market, configure the DEX pool on the - // UniswapOracle, point the SentinelOracle at the UniswapOracle for that token, - // then enable deviation monitoring on the DeviationSentinel. Markets with a - // ZERO_ADDRESS token or pool are skipped (placeholder safety). - for (const market of cfg.monitoredMarkets) { - if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; - commands.push( - { - target: cfg.uniswapOracle, - signature: "setPoolConfig(address,address)", - params: [market.token, market.pool], - dstChainId, - }, - { - target: cfg.sentinelOracle, - signature: "setTokenOracleConfig(address,address)", - params: [market.token, cfg.uniswapOracle], - dstChainId, - }, - { - target: cfg.deviationSentinel, - signature: "setTokenConfig(address,(uint8,bool))", - params: [market.token, [market.deviationPercent, true]], - dstChainId, - }, - ); - } - - return commands; }; export const vip666 = () => { const meta = { version: "v2", - title: "VIP-666 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2", + title: + "VIP-666 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2 — Bootstrap & Permissions (1/2)", description: `#### Description -This VIP configures the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, and **Base**, mirroring the BSC setup from VIP-590 + VIP-610. Each chain's DeviationSentinel routes automated oracle-deviation enforcement through a local EBrakeV2, which applies per-action, per-market restrictions (pause borrow/supply, zero collateral factor) without manual intervention. +This is the first of two VIPs that configure the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, and **Base**, mirroring the BSC setup from VIP-590 + VIP-610. Each chain's DeviationSentinel routes automated oracle-deviation enforcement through a local EBrakeV2, which applies per-action, per-market restrictions (pause borrow/supply, zero collateral factor) without manual intervention. + +The configuration is split across two VIPs so each per-chain payload fits under the destination chain's block gas limit: + +- **VIP-666 (this VIP)** — accept ownership, grant admin/reset/sentinel→ebrake/ebrake→comptroller/multisig permissions, whitelist trusted keepers +- **VIP-667 (follow-up)** — grant Guardian + Timelocks the IL-supported EBrake action permissions, then wire each monitored market on UniswapOracle, SentinelOracle, and DeviationSentinel Because EBrake on these chains uses \`isIsolatedPool=true\` (single-pool IL Comptroller, not the BSC Diamond), only the IL-supported subset of EBrake action functions is granted. Diamond-only functions (\`pauseFlashLoan\`, \`disablePoolBorrow\`, \`revokeFlashLoanAccess\`, \`decreaseCF(address,uint96,uint256)\`) are omitted as they revert on IL comptrollers. @@ -209,25 +185,24 @@ If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: - Grant admin permissions on DeviationSentinel, SentinelOracle, and UniswapOracle to Guardian + 3 Timelocks - Grant **EBrakeV2** the 4 IL-supported Comptroller permissions it needs to execute emergency actions - Authorize **DeviationSentinel** to call \`pauseBorrow\`, \`pauseSupply\`, and \`decreaseCF\` on EBrake -- Grant **Guardian** and governance **Timelocks** the 8 IL-supported EBrake action functions and granular snapshot-reset permissions +- Grant **Guardian** and governance **Timelocks** the granular snapshot-reset permissions on EBrake - Grant the **per-chain 1-of-1 Multisig Pauser** the 8 IL-supported EBrake action functions for manual emergency pausing (Phase 0) - Whitelist Keeper + Guardian + 3 Timelocks as trusted keepers on DeviationSentinel -- Configure deviation monitoring (10% threshold) for the eligible Core Pool markets on each chain — 10 on Ethereum, 5 on Arbitrum One, 4 on Base -**Permission event summary**: 249 PermissionGranted (83 per chain × 3 chains), 0 PermissionRevoked +**Permission event summary**: 153 PermissionGranted (51 per chain × 3 chains), 0 PermissionRevoked #### References - [VIP-590 (BSC)](https://app.venus.io/governance/proposal/590) - [VIP-610 (BSC)](https://app.venus.io/governance/proposal/610) - [Original Proposal: Emergency Brake — Price Deviation Safeguard Mechanism](https://community.venus.io/t/proposal-emergency-brake-price-deviation-safeguard-mechanism/5668) -- [GitHub PR](https://github.com/VenusProtocol/vips/pull/TODO)`, +- [GitHub PR](https://github.com/VenusProtocol/vips/pull/702)`, forDescription: "Execute this proposal", againstDescription: "Do not execute this proposal", abstainDescription: "Indifferent to execution", }; - return makeProposal(NETWORKS.flatMap(buildChainCommands), meta, ProposalType.REGULAR); + return makeProposal(NETWORKS.flatMap(buildChainCommandsA), meta, ProposalType.REGULAR); }; export default vip666; diff --git a/vips/vip-667/bscmainnet.ts b/vips/vip-667/bscmainnet.ts new file mode 100644 index 000000000..d41363337 --- /dev/null +++ b/vips/vip-667/bscmainnet.ts @@ -0,0 +1,96 @@ +import { ZERO_ADDRESS } from "src/networkAddresses"; +import { Command, ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { ARBITRUMONE_CONFIG } from "../vip-666/addresses/arbitrumone"; +import { BASEMAINNET_CONFIG } from "../vip-666/addresses/basemainnet"; +import { ETHEREUM_CONFIG } from "../vip-666/addresses/ethereum"; +import { ChainConfig, GOVERNANCE_EBRAKE_PERMS_IL, governanceAccounts, grant } from "../vip-666/bscmainnet"; + +const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, BASEMAINNET_CONFIG]; + +// VIP-667 (Sub-B): governance EBrake action grants + per-market wiring. +// Per-chain command count: gov ebrake action 32 + 3 × eligible markets. +// - Ethereum: 32 + 27 (9 mkts) = 59 +// - Arbitrum: 32 + 15 (5 mkts) = 47 +// - Base: 32 + 6 (2 mkts) = 38 +// All under their respective block gas limits with the LayerZero adapter param. +const buildChainCommandsB = (cfg: ChainConfig): Command[] => { + const { acm, dstChainId } = cfg; + + const commands: Command[] = [ + // 1. Grant Guardian + governance Timelocks the IL-supported EBrake action functions + ...governanceAccounts(cfg).flatMap(account => + GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, account, dstChainId)), + ), + ]; + + // 2. Per-market wiring. For each eligible market, configure the DEX pool on the + // UniswapOracle, point the SentinelOracle at the UniswapOracle for that token, + // then enable deviation monitoring on the DeviationSentinel. Markets with a + // ZERO_ADDRESS token or pool are skipped (placeholder safety). + for (const market of cfg.monitoredMarkets) { + if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; + commands.push( + { + target: cfg.uniswapOracle, + signature: "setPoolConfig(address,address)", + params: [market.token, market.pool], + dstChainId, + }, + { + target: cfg.sentinelOracle, + signature: "setTokenOracleConfig(address,address)", + params: [market.token, cfg.uniswapOracle], + dstChainId, + }, + { + target: cfg.deviationSentinel, + signature: "setTokenConfig(address,(uint8,bool))", + params: [market.token, [market.deviationPercent, true]], + dstChainId, + }, + ); + } + + return commands; +}; + +export const vip667 = () => { + const meta = { + version: "v2", + title: + "VIP-667 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2 — Governance Actions & Market Wiring (2/2)", + description: `#### Description + +This is the second of two VIPs configuring the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, and **Base**. It depends on VIP-666 (Bootstrap & Permissions) being executed first. + +Splitting the configuration across two VIPs keeps each per-chain payload under the destination chain's block gas limit. VIP-666 covers ownership, admin permissions, reset / sentinel → ebrake / ebrake → comptroller / multisig grants, and trusted-keeper whitelisting. This VIP covers governance EBrake action permissions and per-market deviation wiring. + +Because EBrake on these chains uses \`isIsolatedPool=true\`, only the IL-supported subset of action functions is granted (Diamond-only functions revert on IL comptrollers). + +#### Summary + +If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: + +- Grant **Guardian** and governance **Timelocks** the 8 IL-supported EBrake action functions +- Configure deviation monitoring (10% threshold) for the eligible Core Pool markets on each chain — 9 on Ethereum, 5 on Arbitrum One, 2 on Base + +**Permission event summary**: 96 PermissionGranted (32 per chain × 3 chains), 0 PermissionRevoked + +#### References + +- [VIP-666 (Bootstrap & Permissions)](https://app.venus.io/governance/proposal/666) +- [VIP-590 (BSC)](https://app.venus.io/governance/proposal/590) +- [VIP-610 (BSC)](https://app.venus.io/governance/proposal/610) +- [Original Proposal: Emergency Brake — Price Deviation Safeguard Mechanism](https://community.venus.io/t/proposal-emergency-brake-price-deviation-safeguard-mechanism/5668) +- [GitHub PR](https://github.com/VenusProtocol/vips/pull/702)`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal(NETWORKS.flatMap(buildChainCommandsB), meta, ProposalType.REGULAR); +}; + +export default vip667; From af30061328ecddbb554bb070cad08f498eb5e5f2 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 29 Apr 2026 19:14:48 +0530 Subject: [PATCH 08/15] feat: wire eBTC via Curve, cbBTC + wstETH via Aerodrome Slipstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bootstrap CurveOracle (Ethereum) + AerodromeSlipstreamOracle (Base) in VIP-666: acceptOwnership + setPoolConfig admin grants for Guardian + 3 Timelocks. These markets were previously commented out because their only liquid pools were not Uniswap V3 ABI-compatible — the dedicated adapters from venus-periphery PR #66 unblock them. - Add an `oracleType` discriminator on MonitoredMarket so VIP-667's wiring loop branches between UniswapOracle / CurveOracle (4-arg setPoolConfig with coinIndex + referenceToken) / AerodromeSlipstream (Uniswap-shaped 2-arg setPoolConfig). - Bump VIP-667 fork blocks past the new oracle deployments (Eth ≥ 24985630, Base ≥ 45337626). - Sim note: UniswapOracle and AerodromeSlipstreamOracle share the `PoolConfigUpdated(address,address)` topic, so the post-execution event-count assertion sums Uniswap + Aerodrome markets under one ABI filter; CurveOracle's 4-arg variant is checked separately. --- .../abi/AerodromeSlipstreamOracle.json | 419 ++++++++++++++++ simulations/vip-666/abi/CurveOracle.json | 456 ++++++++++++++++++ simulations/vip-666/arbitrumone.ts | 2 +- simulations/vip-666/basemainnet.ts | 2 +- simulations/vip-666/ethereum.ts | 2 +- simulations/vip-666/shared.ts | 126 ++++- simulations/vip-667/basemainnet.ts | 3 +- simulations/vip-667/ethereum.ts | 3 +- simulations/vip-667/shared.ts | 84 +++- vips/vip-666/addresses/basemainnet.ts | 44 +- vips/vip-666/addresses/ethereum.ts | 36 +- vips/vip-666/bscmainnet.ts | 66 ++- vips/vip-667/bscmainnet.ts | 74 ++- 13 files changed, 1229 insertions(+), 88 deletions(-) create mode 100644 simulations/vip-666/abi/AerodromeSlipstreamOracle.json create mode 100644 simulations/vip-666/abi/CurveOracle.json diff --git a/simulations/vip-666/abi/AerodromeSlipstreamOracle.json b/simulations/vip-666/abi/AerodromeSlipstreamOracle.json new file mode 100644 index 000000000..8c682078e --- /dev/null +++ b/simulations/vip-666/abi/AerodromeSlipstreamOracle.json @@ -0,0 +1,419 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "admin_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "implementation_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "InvalidPool", + "type": "error" + }, + { + "inputs": [], + "name": "TokenNotConfigured", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": 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" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolConfigUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "RESILIENT_ORACLE", + "outputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "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": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "setPoolConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "tokenPools", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_logic", + "type": "address" + }, + { + "internalType": "address", + "name": "admin_", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "constructor" + } +] diff --git a/simulations/vip-666/abi/CurveOracle.json b/simulations/vip-666/abi/CurveOracle.json new file mode 100644 index 000000000..dc913334b --- /dev/null +++ b/simulations/vip-666/abi/CurveOracle.json @@ -0,0 +1,456 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "admin_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "implementation_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "AssetMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "TokenNotConfigured", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroPrice", + "type": "error" + }, + { + "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" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "coinIndex", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "address", + "name": "referenceToken", + "type": "address" + } + ], + "name": "PoolConfigUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "RESILIENT_ORACLE", + "outputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "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": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "poolConfigs", + "outputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint8", + "name": "coinIndex", + "type": "uint8" + }, + { + "internalType": "address", + "name": "referenceToken", + "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": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint8", + "name": "coinIndex", + "type": "uint8" + }, + { + "internalType": "address", + "name": "referenceToken", + "type": "address" + } + ], + "name": "setPoolConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_logic", + "type": "address" + }, + { + "internalType": "address", + "name": "admin_", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "constructor" + } +] diff --git a/simulations/vip-666/arbitrumone.ts b/simulations/vip-666/arbitrumone.ts index e940a72d7..e0c637ba0 100644 --- a/simulations/vip-666/arbitrumone.ts +++ b/simulations/vip-666/arbitrumone.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { ARBITRUMONE_CONFIG } from "../../vips/vip-666/addresses/arbitrumone"; import { runVip666Suite } from "./shared"; -const FORK_BLOCK = 457434103; +const FORK_BLOCK = 457580585; forking(FORK_BLOCK, async () => { await runVip666Suite(ARBITRUMONE_CONFIG); diff --git a/simulations/vip-666/basemainnet.ts b/simulations/vip-666/basemainnet.ts index bb7625916..51bf24bcf 100644 --- a/simulations/vip-666/basemainnet.ts +++ b/simulations/vip-666/basemainnet.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { BASEMAINNET_CONFIG } from "../../vips/vip-666/addresses/basemainnet"; import { runVip666Suite } from "./shared"; -const FORK_BLOCK = 45320649; +const FORK_BLOCK = 45338981; forking(FORK_BLOCK, async () => { await runVip666Suite(BASEMAINNET_CONFIG); diff --git a/simulations/vip-666/ethereum.ts b/simulations/vip-666/ethereum.ts index 406522237..2fa04139b 100644 --- a/simulations/vip-666/ethereum.ts +++ b/simulations/vip-666/ethereum.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; import { runVip666Suite } from "./shared"; -const FORK_BLOCK = 24982817; +const FORK_BLOCK = 24985871; forking(FORK_BLOCK, async () => { await runVip666Suite(ETHEREUM_CONFIG); diff --git a/simulations/vip-666/shared.ts b/simulations/vip-666/shared.ts index 5ea637dc8..d076f8ac6 100644 --- a/simulations/vip-666/shared.ts +++ b/simulations/vip-666/shared.ts @@ -7,6 +7,8 @@ import { expectEvents, initMainnetUser } from "src/utils"; import { testForkedNetworkVipCommands } from "src/vip-framework"; import vip666, { + AERODROME_ORACLE_ADMIN_PERMS, + CURVE_ORACLE_ADMIN_PERMS, ChainConfig, DIAMOND_ONLY_EBRAKE_PERMS, EBRAKE_COMPTROLLER_PERMS_IL, @@ -19,15 +21,29 @@ import vip666, { governanceAccounts, } from "../../vips/vip-666/bscmainnet"; import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; +import AERODROME_ORACLE_ABI from "./abi/AerodromeSlipstreamOracle.json"; +import CURVE_ORACLE_ABI from "./abi/CurveOracle.json"; import DEVIATION_SENTINEL_ABI from "./abi/DeviationSentinel.json"; import EBRAKE_ABI from "./abi/EBrake.json"; import SENTINEL_ORACLE_ABI from "./abi/SentinelOracle.json"; import UNISWAP_ORACLE_ABI from "./abi/UniswapOracle.json"; -// RoleGranted events emitted by VIP-666 (Sub-A) per chain. -// Sum of: admin grants (12+8+4) + ebrake→comptroller (4) + reset (12) + sentinel→ebrake (3) -// + multisig ebrake action (8) = 51. See buildChainCommandsA in vips/vip-666/bscmainnet.ts. -const PERMS_GRANTED_PER_CHAIN = 51; +// RoleGranted events emitted by VIP-666 (Sub-A). Per-chain count is variable because +// CurveOracle (Ethereum) and AerodromeSlipstreamOracle (Base) each add 4 admin grants. +// base = admin grants (12+8+4) + ebrake→comptroller (4) + reset (12) + sentinel→ebrake (3) + multisig (8) = 51 +// +4 if cfg.curveOracle is set, +4 if cfg.aerodromeOracle is set +const expectedPermsGranted = (cfg: ChainConfig): number => { + let n = 51; + if (cfg.curveOracle) n += 4; + if (cfg.aerodromeOracle) n += 4; + return n; +}; +const expectedAcceptOwnership = (cfg: ChainConfig): number => { + let n = 4; + if (cfg.curveOracle) n += 1; + if (cfg.aerodromeOracle) n += 1; + return n; +}; const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { const missing: string[] = []; @@ -58,12 +74,16 @@ export const runVip666Suite = async (cfg: ChainConfig) => { let eBrake: Contract; let sentinelOracle: Contract; let uniswapOracle: Contract; + let curveOracle: Contract | undefined; + let aerodromeOracle: Contract | undefined; // ACM.isAllowedToCall(account, sig) checks msg.sender as the host contract. // Impersonate each host so the role lookup resolves correctly. let impersonatedDeviationSentinel: SignerWithAddress; let impersonatedSentinelOracle: SignerWithAddress; let impersonatedUniswapOracle: SignerWithAddress; + let impersonatedCurveOracle: SignerWithAddress | undefined; + let impersonatedAerodromeOracle: SignerWithAddress | undefined; let impersonatedComptroller: SignerWithAddress; const govAccounts = governanceAccounts(cfg); @@ -80,6 +100,15 @@ export const runVip666Suite = async (cfg: ChainConfig) => { impersonatedSentinelOracle = await initMainnetUser(cfg.sentinelOracle, ethers.utils.parseEther("1")); impersonatedUniswapOracle = await initMainnetUser(cfg.uniswapOracle, ethers.utils.parseEther("1")); impersonatedComptroller = await initMainnetUser(cfg.comptroller, ethers.utils.parseEther("1")); + + if (cfg.curveOracle) { + curveOracle = await ethers.getContractAt(CURVE_ORACLE_ABI, cfg.curveOracle); + impersonatedCurveOracle = await initMainnetUser(cfg.curveOracle, ethers.utils.parseEther("1")); + } + if (cfg.aerodromeOracle) { + aerodromeOracle = await ethers.getContractAt(AERODROME_ORACLE_ABI, cfg.aerodromeOracle); + impersonatedAerodromeOracle = await initMainnetUser(cfg.aerodromeOracle, ethers.utils.parseEther("1")); + } }); describe(`VIP-666 [${cfg.name}] — Pre-VIP behaviour`, () => { @@ -99,6 +128,18 @@ export const runVip666Suite = async (cfg: ChainConfig) => { expect(await eBrake.pendingOwner()).to.equal(cfg.normalTimelock); }); + if (cfg.curveOracle) { + it("CurveOracle pendingOwner is Normal Timelock", async () => { + expect(await curveOracle!.pendingOwner()).to.equal(cfg.normalTimelock); + }); + } + + if (cfg.aerodromeOracle) { + it("AerodromeSlipstreamOracle pendingOwner is Normal Timelock", async () => { + expect(await aerodromeOracle!.pendingOwner()).to.equal(cfg.normalTimelock); + }); + } + it("EBrake immutables are correctly set for IL", async () => { expect(await eBrake.COMPTROLLER()).to.equal(cfg.comptroller); expect(await eBrake.IS_ISOLATED_POOL()).to.equal(true); @@ -136,6 +177,28 @@ export const runVip666Suite = async (cfg: ChainConfig) => { } }); + if (cfg.curveOracle) { + it("Guardian + Timelocks have no admin permissions on CurveOracle yet", async () => { + const a = acm.connect(impersonatedCurveOracle!); + for (const account of govAccounts) { + for (const sig of CURVE_ORACLE_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + } + + if (cfg.aerodromeOracle) { + it("Guardian + Timelocks have no admin permissions on AerodromeSlipstreamOracle yet", async () => { + const a = acm.connect(impersonatedAerodromeOracle!); + for (const account of govAccounts) { + for (const sig of AERODROME_ORACLE_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + } + it("EBrake has no permissions on the IL Comptroller yet", async () => { const a = acm.connect(impersonatedComptroller); for (const sig of EBRAKE_COMPTROLLER_PERMS_IL) { @@ -172,28 +235,38 @@ export const runVip666Suite = async (cfg: ChainConfig) => { testForkedNetworkVipCommands(`VIP-666 [${cfg.name}] Bootstrap & Permissions`, await vip666(), { callbackAfterExecution: async txResponse => { - // 4 acceptOwnership() calls per chain - await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["OwnershipTransferred"], [4]); + // 4 + (1 each for CurveOracle / AerodromeSlipstreamOracle) acceptOwnership() calls + await expectEvents( + txResponse, + [DEVIATION_SENTINEL_ABI], + ["OwnershipTransferred"], + [expectedAcceptOwnership(cfg)], + ); // 5 trusted keepers whitelisted per chain await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TrustedKeeperUpdated"], [5]); - // 51 RoleGranted events per chain (Sub-A only — governance EBrake actions deferred to VIP-667) - await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [PERMS_GRANTED_PER_CHAIN]); + // RoleGranted events (Sub-A only — governance EBrake actions deferred to VIP-667). + // Variable per chain depending on which optional DEX oracles are present. + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [expectedPermsGranted(cfg)]); }, }); describe(`VIP-666 [${cfg.name}] — Post-VIP behaviour`, () => { - it("All four contracts have Normal Timelock as owner", async () => { + it("All bootstrap contracts have Normal Timelock as owner", async () => { expect(await deviationSentinel.owner()).to.equal(cfg.normalTimelock); expect(await sentinelOracle.owner()).to.equal(cfg.normalTimelock); expect(await uniswapOracle.owner()).to.equal(cfg.normalTimelock); expect(await eBrake.owner()).to.equal(cfg.normalTimelock); + if (cfg.curveOracle) expect(await curveOracle!.owner()).to.equal(cfg.normalTimelock); + if (cfg.aerodromeOracle) expect(await aerodromeOracle!.owner()).to.equal(cfg.normalTimelock); }); - it("All four contracts have AddressZero as pendingOwner (acceptOwnership cleared it)", async () => { + it("All bootstrap contracts have AddressZero as pendingOwner (acceptOwnership cleared it)", async () => { expect(await deviationSentinel.pendingOwner()).to.equal(ZERO_ADDRESS); expect(await sentinelOracle.pendingOwner()).to.equal(ZERO_ADDRESS); expect(await uniswapOracle.pendingOwner()).to.equal(ZERO_ADDRESS); expect(await eBrake.pendingOwner()).to.equal(ZERO_ADDRESS); + if (cfg.curveOracle) expect(await curveOracle!.pendingOwner()).to.equal(ZERO_ADDRESS); + if (cfg.aerodromeOracle) expect(await aerodromeOracle!.pendingOwner()).to.equal(ZERO_ADDRESS); }); it("Guardian + Timelocks have all admin permissions on DeviationSentinel", async () => { @@ -223,6 +296,28 @@ export const runVip666Suite = async (cfg: ChainConfig) => { } }); + if (cfg.curveOracle) { + it("Guardian + Timelocks have setPoolConfig permission on CurveOracle", async () => { + const a = acm.connect(impersonatedCurveOracle!); + for (const account of govAccounts) { + for (const sig of CURVE_ORACLE_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + } + + if (cfg.aerodromeOracle) { + it("Guardian + Timelocks have setPoolConfig permission on AerodromeSlipstreamOracle", async () => { + const a = acm.connect(impersonatedAerodromeOracle!); + for (const account of govAccounts) { + for (const sig of AERODROME_ORACLE_ADMIN_PERMS) { + expect(await a.isAllowedToCall(account, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + } + it("EBrake has all four IL-supported permissions on the local IL Comptroller", async () => { const a = acm.connect(impersonatedComptroller); for (const sig of EBRAKE_COMPTROLLER_PERMS_IL) { @@ -283,7 +378,16 @@ export const runVip666Suite = async (cfg: ChainConfig) => { for (const market of cfg.monitoredMarkets) { if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; it(`${market.symbol} is still not wired (deferred to VIP-667)`, async () => { - expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + // Each market's wiring lives on its routed DEX oracle; assert its slot is empty. + const oracleType = market.oracleType ?? "uniswap"; + if (oracleType === "curve") { + const cfgEntry = await curveOracle!.poolConfigs(market.token); + expect(cfgEntry.pool).to.equal(ZERO_ADDRESS); + } else if (oracleType === "aerodrome") { + expect(await aerodromeOracle!.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + } else { + expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + } const tcSentinel = await sentinelOracle.tokenConfigs(market.token); expect(tcSentinel.oracle ?? tcSentinel).to.equal(ZERO_ADDRESS); const tcDev = await deviationSentinel.tokenConfigs(market.token); diff --git a/simulations/vip-667/basemainnet.ts b/simulations/vip-667/basemainnet.ts index 7f9e5eef9..68df03a5e 100644 --- a/simulations/vip-667/basemainnet.ts +++ b/simulations/vip-667/basemainnet.ts @@ -3,7 +3,8 @@ import { forking } from "src/vip-framework"; import { BASEMAINNET_CONFIG } from "../../vips/vip-666/addresses/basemainnet"; import { runVip667Suite } from "./shared"; -const FORK_BLOCK = 45320649; +// Must be ≥ 45337626 — AerodromeSlipstreamOracle proxy deployment block. +const FORK_BLOCK = 45338981; forking(FORK_BLOCK, async () => { await runVip667Suite(BASEMAINNET_CONFIG); diff --git a/simulations/vip-667/ethereum.ts b/simulations/vip-667/ethereum.ts index 44edee294..07d4ac98e 100644 --- a/simulations/vip-667/ethereum.ts +++ b/simulations/vip-667/ethereum.ts @@ -3,7 +3,8 @@ import { forking } from "src/vip-framework"; import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; import { runVip667Suite } from "./shared"; -const FORK_BLOCK = 24982817; +// Must be ≥ 24985630 — CurveOracle proxy deployment block. +const FORK_BLOCK = 24985871; forking(FORK_BLOCK, async () => { await runVip667Suite(ETHEREUM_CONFIG); diff --git a/simulations/vip-667/shared.ts b/simulations/vip-667/shared.ts index 10b3ea21d..863be6d16 100644 --- a/simulations/vip-667/shared.ts +++ b/simulations/vip-667/shared.ts @@ -5,9 +5,16 @@ import { ZERO_ADDRESS } from "src/networkAddresses"; import { expectEvents, getForkedNetworkAddress, setMaxStalePeriodInChainlinkOracle } from "src/utils"; import { testForkedNetworkVipCommands } from "src/vip-framework"; -import vip666, { ChainConfig, GOVERNANCE_EBRAKE_PERMS_IL, governanceAccounts } from "../../vips/vip-666/bscmainnet"; +import vip666, { + ChainConfig, + GOVERNANCE_EBRAKE_PERMS_IL, + MonitoredMarket, + governanceAccounts, +} from "../../vips/vip-666/bscmainnet"; import vip667 from "../../vips/vip-667/bscmainnet"; import ACCESS_CONTROL_MANAGER_ABI from "../vip-666/abi/AccessControlManager.json"; +import AERODROME_ORACLE_ABI from "../vip-666/abi/AerodromeSlipstreamOracle.json"; +import CURVE_ORACLE_ABI from "../vip-666/abi/CurveOracle.json"; import DEVIATION_SENTINEL_ABI from "../vip-666/abi/DeviationSentinel.json"; import IL_COMPTROLLER_ABI from "../vip-666/abi/ILComptroller.json"; import RESILIENT_ORACLE_ABI from "../vip-666/abi/ResilientOracle.json"; @@ -19,12 +26,31 @@ import VTOKEN_ABI from "../vip-666/abi/VToken.json"; // 8 (governance ebrake action) × 4 = 32 (token wiring uses direct setter calls) const PERMS_GRANTED_PER_CHAIN = 32; +// Resolve the DEX-oracle address used to price a market based on its oracleType. +// Mirrors `resolveDexOracle` in the VIP itself — kept inline (not imported) because +// the VIP module throws on misconfig and the simulation's expressed intent is checking +// post-state, not duplicating proposal-build validation. +const dexOracleFor = (cfg: ChainConfig, market: MonitoredMarket): string => { + switch (market.oracleType ?? "uniswap") { + case "uniswap": + return cfg.uniswapOracle; + case "curve": + return cfg.curveOracle as string; + case "aerodrome": + return cfg.aerodromeOracle as string; + } +}; + +const countMarketsByOracle = (markets: MonitoredMarket[], type: MonitoredMarket["oracleType"]) => + markets.filter(m => (m.oracleType ?? "uniswap") === (type ?? "uniswap")).length; + // Tokens whose `DeviationSentinel.checkPriceDeviation` end-to-end check is fragile at // fork time only — typically wrapped/correlated oracles whose inner feeds expose // staleness windows we can't override from the simulation side. Production behaviour // is unaffected (verified at live timestamps). // - LBTC: main oracle is OneJumpOracle (RedStone LBTC/BTC ratio + Chainlink BTC/USD). -const SKIP_CHECK_PRICE_DEVIATION = new Set(["LBTC"]); +// - eBTC: same OneJumpOracle pattern (eBTC/BTC ratio + BTC/USD), so matches LBTC's profile. +const SKIP_CHECK_PRICE_DEVIATION = new Set(["LBTC", "eBTC"]); const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { const missing: string[] = []; @@ -99,6 +125,8 @@ export const runVip667Suite = async (cfg: ChainConfig) => { let deviationSentinel: Contract; let sentinelOracle: Contract; let uniswapOracle: Contract; + let curveOracle: Contract | undefined; + let aerodromeOracle: Contract | undefined; let resilientOracle: Contract; // underlying address (lowercased) → vToken address — built by walking IL Comptroller markets. let vTokenByUnderlying: Map; @@ -110,6 +138,8 @@ export const runVip667Suite = async (cfg: ChainConfig) => { deviationSentinel = await ethers.getContractAt(DEVIATION_SENTINEL_ABI, cfg.deviationSentinel); sentinelOracle = await ethers.getContractAt(SENTINEL_ORACLE_ABI, cfg.sentinelOracle); uniswapOracle = await ethers.getContractAt(UNISWAP_ORACLE_ABI, cfg.uniswapOracle); + if (cfg.curveOracle) curveOracle = await ethers.getContractAt(CURVE_ORACLE_ABI, cfg.curveOracle); + if (cfg.aerodromeOracle) aerodromeOracle = await ethers.getContractAt(AERODROME_ORACLE_ABI, cfg.aerodromeOracle); resilientOracle = await ethers.getContractAt(RESILIENT_ORACLE_ABI, getForkedNetworkAddress("RESILIENT_ORACLE")); await bumpAdapterStaleness(cfg); @@ -156,7 +186,19 @@ export const runVip667Suite = async (cfg: ChainConfig) => { for (const market of cfg.monitoredMarkets) { it(`${market.symbol} is not wired yet`, async () => { - expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + // Pre-VIP, the appropriate DEX oracle has no pool entry for this token. + // Each oracle uses a different storage shape for its pool config: + // - UniswapOracle / AerodromeSlipstreamOracle: tokenPools(token) -> address + // - CurveOracle: poolConfigs(token) -> { pool, coinIndex, referenceToken } + const oracleType = market.oracleType ?? "uniswap"; + if (oracleType === "curve") { + const cfgEntry = await curveOracle!.poolConfigs(market.token); + expect(cfgEntry.pool).to.equal(ZERO_ADDRESS); + } else if (oracleType === "aerodrome") { + expect(await aerodromeOracle!.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + } else { + expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + } const tcSentinel = await sentinelOracle.tokenConfigs(market.token); expect(tcSentinel.oracle ?? tcSentinel).to.equal(ZERO_ADDRESS); const tcDev = await deviationSentinel.tokenConfigs(market.token); @@ -170,8 +212,17 @@ export const runVip667Suite = async (cfg: ChainConfig) => { callbackAfterExecution: async txResponse => { // 32 RoleGranted events per chain (governance EBrake action perms) await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [PERMS_GRANTED_PER_CHAIN]); - // 1 wiring event per market on each of UniswapOracle, SentinelOracle, DeviationSentinel - await expectEvents(txResponse, [UNISWAP_ORACLE_ABI], ["PoolConfigUpdated"], [cfg.monitoredMarkets.length]); + // PoolConfigUpdated counts. UniswapOracle and AerodromeSlipstreamOracle emit the + // identical 2-arg `PoolConfigUpdated(address,address)` event (same topic hash), + // so a UniswapOracle-ABI filter decodes both — assert against the combined count. + // CurveOracle emits its own 4-arg variant (distinct topic), checked separately. + const uniswapMarkets = countMarketsByOracle(cfg.monitoredMarkets, "uniswap"); + const curveMarkets = countMarketsByOracle(cfg.monitoredMarkets, "curve"); + const aerodromeMarkets = countMarketsByOracle(cfg.monitoredMarkets, "aerodrome"); + await expectEvents(txResponse, [UNISWAP_ORACLE_ABI], ["PoolConfigUpdated"], [uniswapMarkets + aerodromeMarkets]); + if (cfg.curveOracle && curveMarkets > 0) { + await expectEvents(txResponse, [CURVE_ORACLE_ABI], ["PoolConfigUpdated"], [curveMarkets]); + } await expectEvents( txResponse, [SENTINEL_ORACLE_ABI], @@ -192,15 +243,30 @@ export const runVip667Suite = async (cfg: ChainConfig) => { }); for (const market of cfg.monitoredMarkets) { - it(`${market.symbol} pool is configured on UniswapOracle`, async () => { - const actual = await uniswapOracle.tokenPools(market.token); - expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); + const expectedDexOracle = dexOracleFor(cfg, market); + + it(`${market.symbol} pool is configured on the routed DEX oracle`, async () => { + const oracleType = market.oracleType ?? "uniswap"; + if (oracleType === "curve") { + const cfgEntry = await curveOracle!.poolConfigs(market.token); + expect(ethers.utils.getAddress(cfgEntry.pool)).to.equal(ethers.utils.getAddress(market.pool)); + expect(cfgEntry.coinIndex).to.equal(market.coinIndex); + expect(ethers.utils.getAddress(cfgEntry.referenceToken)).to.equal( + ethers.utils.getAddress(market.referenceToken as string), + ); + } else if (oracleType === "aerodrome") { + const actual = await aerodromeOracle!.tokenPools(market.token); + expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); + } else { + const actual = await uniswapOracle.tokenPools(market.token); + expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); + } }); it(`${market.symbol} oracle is configured on SentinelOracle`, async () => { const tc = await sentinelOracle.tokenConfigs(market.token); const actual = tc.oracle ?? tc; - expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(cfg.uniswapOracle)); + expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(expectedDexOracle)); }); it(`${market.symbol} deviation threshold is configured on DeviationSentinel`, async () => { diff --git a/vips/vip-666/addresses/basemainnet.ts b/vips/vip-666/addresses/basemainnet.ts index d1386a586..170a2032b 100644 --- a/vips/vip-666/addresses/basemainnet.ts +++ b/vips/vip-666/addresses/basemainnet.ts @@ -16,13 +16,17 @@ export const BASEMAINNET_DEVIATION_SENTINEL = "0x12D09d5b13A673269cdB624D17A42f4 export const BASEMAINNET_EBRAKE = "0x062C68Af7B9Fb059DCB7FA4B6b92E633350fb7c2"; export const BASEMAINNET_SENTINEL_ORACLE = "0xCdD6D79Fd313C21967CED04C1b8bE70BDc27574D"; export const BASEMAINNET_UNISWAP_ORACLE = "0xc3b5169a7d5f6341403c74187Db3C4Fe6d447762"; +// Deployed via venus-periphery PR #66 — prices Aerodrome Slipstream (CL) pools whose +// slot0() 6-tuple is incompatible with the UniswapOracle V3 ABI. +export const BASEMAINNET_AERODROME_ORACLE = "0x5DE0B322A74088fD64CDD01042BE2fBc47FE82EC"; export const BASEMAINNET_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig export const BASEMAINNET_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; export const BASEMAINNET_DST_CHAIN_ID = LzChainId.basemainnet; -// Eligible Core Pool markets — Uniswap V3 Base sources only, unified 10% threshold. -// cbBTC and wstETH are excluded — see commented entries below for rationale. +// Eligible Core Pool markets — unified 10% threshold. WETH and USDC route through the +// most liquid Uniswap V3 Base pool; cbBTC and wstETH route through the AerodromeSlipstreamOracle +// because their only liquid pools live on Aerodrome's concentrated-liquidity DEX. export const BASEMAINNET_MONITORED_MARKETS = [ { symbol: "WETH", @@ -36,27 +40,20 @@ export const BASEMAINNET_MONITORED_MARKETS = [ pool: "0x6c561b446416e1a00e8e93e221854d6ea4171372", // USDC/WETH Uniswap V3 Base deviationPercent: 10, }, - // cbBTC and wstETH are intentionally NOT wired in this VIP. The only liquid pools - // for these tokens on Base are Aerodrome Slipstream (cbBTC/USDC `0x4e96…e778`, - // wstETH/WETH `0x861a…1b5f`). Aerodrome Slipstream's `slot0()` returns a 6-tuple - // (no `feeProtocol`), but Venus' UniswapOracle uses the Uniswap V3 7-tuple - // `IUniswapV3Pool.slot0()` ABI — the Solidity decoder reverts when reading past - // the end of the Slipstream return data. handleDeviation would silently fail for - // these markets in production. Re-include in a follow-up VIP once either: - // (a) a Slipstream-compatible oracle adapter is deployed in venus-periphery, or - // (b) the market team selects a Uniswap V3 Base pool with adequate liquidity. - // { - // symbol: "cbBTC", - // token: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", - // pool: "0x4e962bb3889bf030368f56810a9c96b83cb3e778", - // deviationPercent: 10, - // }, - // { - // symbol: "wstETH", - // token: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", - // pool: "0x861a2922be165a5bd41b1e482b49216b465e1b5f", - // deviationPercent: 10, - // }, + { + symbol: "cbBTC", + token: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + pool: "0x4e962bb3889bf030368f56810a9c96b83cb3e778", // cbBTC/USDC Aerodrome Slipstream + deviationPercent: 10, + oracleType: "aerodrome" as const, + }, + { + symbol: "wstETH", + token: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + pool: "0x861a2922be165a5bd41b1e482b49216b465e1b5f", // wstETH/WETH Aerodrome Slipstream + deviationPercent: 10, + oracleType: "aerodrome" as const, + }, ]; export const BASEMAINNET_CONFIG = { @@ -72,6 +69,7 @@ export const BASEMAINNET_CONFIG = { eBrake: BASEMAINNET_EBRAKE, sentinelOracle: BASEMAINNET_SENTINEL_ORACLE, uniswapOracle: BASEMAINNET_UNISWAP_ORACLE, + aerodromeOracle: BASEMAINNET_AERODROME_ORACLE, multisigPauser: BASEMAINNET_MULTISIG_PAUSER, keeper: BASEMAINNET_KEEPER, monitoredMarkets: BASEMAINNET_MONITORED_MARKETS, diff --git a/vips/vip-666/addresses/ethereum.ts b/vips/vip-666/addresses/ethereum.ts index add9e5dfb..0328a3cf8 100644 --- a/vips/vip-666/addresses/ethereum.ts +++ b/vips/vip-666/addresses/ethereum.ts @@ -16,14 +16,19 @@ export const ETHEREUM_DEVIATION_SENTINEL = "0x7D0EFA41eBF1aF242A37174E1E047bD6ea export const ETHEREUM_EBRAKE = "0xCD09042c5DFFed762998Df9a058ec5944e39949B"; export const ETHEREUM_SENTINEL_ORACLE = "0x444C53E194B40c272fAd683210e2cB1c16Ab132e"; export const ETHEREUM_UNISWAP_ORACLE = "0x873993F8f5f5Ddbae0952e939ab3005Af363Af00"; +// Deployed via venus-periphery PR #66 — prices Curve StableSwap-NG assets that +// UniswapOracle can't read (eBTC/WBTC pool, etc.). +export const ETHEREUM_CURVE_ORACLE = "0x64a811bd0E91cf00D9CE0769eDA028026577A6D9"; export const ETHEREUM_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig export const ETHEREUM_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; +// Reference token for eBTC's CurveOracle entry — see CurveOracle.sol for the pricing math. +const WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"; + export const ETHEREUM_DST_CHAIN_ID = LzChainId.ethereum; -// Eligible Core Pool markets — Uniswap V3 sources, unified 10% threshold. -// crvUSD and EIGEN are intentionally excluded per market spec. -// eBTC is also excluded — see commented entry at the bottom of the array for rationale. +// Eligible Core Pool markets — Uniswap V3 sources except eBTC (CurveOracle). +// Unified 10% threshold. crvUSD and EIGEN are intentionally excluded per market spec. export const ETHEREUM_MONITORED_MARKETS = [ { symbol: "WETH", @@ -61,19 +66,17 @@ export const ETHEREUM_MONITORED_MARKETS = [ pool: "0xe6d7ebb9f1a9519dc06d557e03c522d53520e76a", // USDe/USDC Uniswap V3 deviationPercent: 10, }, - // eBTC is intentionally NOT wired in this VIP. The only liquid eBTC pool on Ethereum - // (0x7704…2d6) is a Curve StableSwap NG pool (Vyper), not a Uniswap V3 pool. - // UniswapOracle reads token0()/token1(), which revert on the Curve contract, so - // handleDeviation would always revert for eBTC and the keeper could never trigger - // monitoring for this market. Re-include in a follow-up VIP once either: - // (a) a CurveOracle is deployed and wired into SentinelOracle for eBTC, or - // (b) market team selects a V3-compatible eBTC pool to monitor instead. - // { - // symbol: "eBTC", - // token: "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", - // pool: "0x7704d01908afd31bf647d969c295bb45230cd2d6", - // deviationPercent: 10, - // }, + // eBTC routes through CurveOracle (eBTC/WBTC StableSwap-NG pool). coinIndex=0 because + // eBTC is coins[0] on this pool; referenceToken is the paired token (WBTC). + { + symbol: "eBTC", + token: "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", + pool: "0x7704d01908afd31bf647d969c295bb45230cd2d6", + deviationPercent: 10, + oracleType: "curve" as const, + coinIndex: 0, + referenceToken: WBTC, + }, { symbol: "DAI", token: "0x6B175474E89094C44Da98b954EedeAC495271d0F", @@ -107,6 +110,7 @@ export const ETHEREUM_CONFIG = { eBrake: ETHEREUM_EBRAKE, sentinelOracle: ETHEREUM_SENTINEL_ORACLE, uniswapOracle: ETHEREUM_UNISWAP_ORACLE, + curveOracle: ETHEREUM_CURVE_ORACLE, multisigPauser: ETHEREUM_MULTISIG_PAUSER, keeper: ETHEREUM_KEEPER, monitoredMarkets: ETHEREUM_MONITORED_MARKETS, diff --git a/vips/vip-666/bscmainnet.ts b/vips/vip-666/bscmainnet.ts index 680ed7e2f..4379ea84a 100644 --- a/vips/vip-666/bscmainnet.ts +++ b/vips/vip-666/bscmainnet.ts @@ -5,11 +5,21 @@ import { ARBITRUMONE_CONFIG } from "./addresses/arbitrumone"; import { BASEMAINNET_CONFIG } from "./addresses/basemainnet"; import { ETHEREUM_CONFIG } from "./addresses/ethereum"; +export type OracleType = "uniswap" | "curve" | "aerodrome"; + export interface MonitoredMarket { symbol: string; token: string; pool: string; deviationPercent: number; + // Defaults to "uniswap" for the existing UniswapOracle path. "curve" routes through + // CurveOracle and requires coinIndex + referenceToken; "aerodrome" routes through + // AerodromeSlipstreamOracle and uses the Uniswap-shaped (token, pool) signature. + oracleType?: OracleType; + // Curve-only: index of the asset in the StableSwap-NG pool's coins() array. + coinIndex?: number; + // Curve-only: the paired token whose USD price is fetched from ResilientOracle. + referenceToken?: string; } export interface ChainConfig { @@ -25,6 +35,10 @@ export interface ChainConfig { eBrake: string; sentinelOracle: string; uniswapOracle: string; + // Optional, per-chain DEX oracles. Present only on chains that need them; bootstrap + // permissions + ownership transfer are conditionally added when set. + curveOracle?: string; + aerodromeOracle?: string; multisigPauser: string; keeper: string; monitoredMarkets: MonitoredMarket[]; @@ -52,6 +66,13 @@ export const SENTINEL_ORACLE_ADMIN_PERMS = ["setTokenOracleConfig(address,addres // UniswapOracle access-controlled functions export const UNISWAP_ORACLE_ADMIN_PERMS = ["setPoolConfig(address,address)"]; +// CurveOracle access-controlled functions — distinct setPoolConfig signature +// (StableSwap-NG needs coinIndex + referenceToken in addition to token + pool). +export const CURVE_ORACLE_ADMIN_PERMS = ["setPoolConfig(address,address,uint8,address)"]; + +// AerodromeSlipstreamOracle access-controlled functions — same shape as UniswapOracle. +export const AERODROME_ORACLE_ADMIN_PERMS = ["setPoolConfig(address,address)"]; + // IL Comptroller permissions for EBrake. // setActionsPaused uses uint256[] (not uint8[]) in the IL Comptroller. export const EBRAKE_COMPTROLLER_PERMS_IL = [ @@ -104,17 +125,25 @@ export const grant = (acm: string, contract: string, sig: string, account: strin // VIP-666 (Sub-A): bootstrap + permissions. Kept under each chain's block gas // limit by deferring governance EBrake action grants and market wiring to VIP-667. -// Per-chain command count: 60 (acceptOwnership 4 + admin grants 24 + ebrake→comptroller 4 -// + reset 12 + sentinel→ebrake 3 + multisig ebrake action 8 + trusted keepers 5). +// Per-chain command count varies with the optional CurveOracle (Ethereum) and +// AerodromeSlipstreamOracle (Base): each adds 1 acceptOwnership + 4 admin grants. +// - Ethereum: 60 + 5 (CurveOracle) = 65 +// - Arbitrum: 60 = 60 +// - Base: 60 + 5 (AerodromeSlipstream) = 65 const buildChainCommandsA = (cfg: ChainConfig): Command[] => { const { acm, dstChainId } = cfg; const govAccounts = governanceAccounts(cfg); const trustedKeeperAccounts = [cfg.keeper, ...govAccounts]; + const ownershipTargets: string[] = [cfg.deviationSentinel, cfg.sentinelOracle, cfg.uniswapOracle, cfg.eBrake]; + if (cfg.curveOracle) ownershipTargets.push(cfg.curveOracle); + if (cfg.aerodromeOracle) ownershipTargets.push(cfg.aerodromeOracle); + return [ - // 1. Accept ownership of the four newly deployed contracts. The deployer - // transfers ownership to the local Normal Timelock prior to this VIP. - ...[cfg.deviationSentinel, cfg.sentinelOracle, cfg.uniswapOracle, cfg.eBrake].map(target => ({ + // 1. Accept ownership of the newly deployed contracts. The deployer transfers + // ownership to the local Normal Timelock prior to this VIP. Always 4 (the + // base stack); +1 each for CurveOracle / AerodromeSlipstreamOracle when present. + ...ownershipTargets.map(target => ({ target, signature: "acceptOwnership()", params: [], @@ -136,6 +165,20 @@ const buildChainCommandsA = (cfg: ChainConfig): Command[] => { UNISWAP_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.uniswapOracle, sig, account, dstChainId)), ), + // 4b. Grant Guardian + governance Timelocks admin permission on CurveOracle (when present) + ...(cfg.curveOracle + ? govAccounts.flatMap(account => + CURVE_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.curveOracle as string, sig, account, dstChainId)), + ) + : []), + + // 4c. Grant Guardian + governance Timelocks admin permission on AerodromeSlipstreamOracle (when present) + ...(cfg.aerodromeOracle + ? govAccounts.flatMap(account => + AERODROME_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.aerodromeOracle as string, sig, account, dstChainId)), + ) + : []), + // 5. Grant EBrake the IL-Comptroller-supported emergency-action permissions ...EBRAKE_COMPTROLLER_PERMS_IL.map(sig => grant(acm, cfg.comptroller, sig, cfg.eBrake, dstChainId)), @@ -173,7 +216,12 @@ This is the first of two VIPs that configure the **DeviationSentinel** + **EBrak The configuration is split across two VIPs so each per-chain payload fits under the destination chain's block gas limit: - **VIP-666 (this VIP)** — accept ownership, grant admin/reset/sentinel→ebrake/ebrake→comptroller/multisig permissions, whitelist trusted keepers -- **VIP-667 (follow-up)** — grant Guardian + Timelocks the IL-supported EBrake action permissions, then wire each monitored market on UniswapOracle, SentinelOracle, and DeviationSentinel +- **VIP-667 (follow-up)** — grant Guardian + Timelocks the IL-supported EBrake action permissions, then wire each monitored market on the appropriate DEX oracle (UniswapOracle / CurveOracle / AerodromeSlipstreamOracle), SentinelOracle, and DeviationSentinel + +In addition to UniswapOracle, two extra DEX oracles are bootstrapped on the chains that need them: + +- **CurveOracle (Ethereum only)** — prices eBTC against WBTC via the eBTC/WBTC Curve StableSwap-NG pool's EMA \`price_oracle\`. Required because Curve StableSwap-NG pools are not Uniswap V3 ABI-compatible. +- **AerodromeSlipstreamOracle (Base only)** — prices cbBTC and wstETH on the most liquid Aerodrome Slipstream pools (cbBTC/USDC and wstETH/WETH). Aerodrome Slipstream's \`slot0()\` returns a 6-tuple (no \`feeProtocol\`) so the Solidity decoder reverts when read against the Uniswap V3 7-tuple ABI used by UniswapOracle. Because EBrake on these chains uses \`isIsolatedPool=true\` (single-pool IL Comptroller, not the BSC Diamond), only the IL-supported subset of EBrake action functions is granted. Diamond-only functions (\`pauseFlashLoan\`, \`disablePoolBorrow\`, \`revokeFlashLoanAccess\`, \`decreaseCF(address,uint96,uint256)\`) are omitted as they revert on IL comptrollers. @@ -181,15 +229,15 @@ Because EBrake on these chains uses \`isIsolatedPool=true\` (single-pool IL Comp If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: -- Accept governance ownership of the **DeviationSentinel**, **SentinelOracle**, **UniswapOracle**, and **EBrakeV2** contracts -- Grant admin permissions on DeviationSentinel, SentinelOracle, and UniswapOracle to Guardian + 3 Timelocks +- Accept governance ownership of the **DeviationSentinel**, **SentinelOracle**, **UniswapOracle**, and **EBrakeV2** contracts (plus **CurveOracle** on Ethereum and **AerodromeSlipstreamOracle** on Base) +- Grant admin permissions on DeviationSentinel, SentinelOracle, UniswapOracle, and (where present) CurveOracle / AerodromeSlipstreamOracle to Guardian + 3 Timelocks - Grant **EBrakeV2** the 4 IL-supported Comptroller permissions it needs to execute emergency actions - Authorize **DeviationSentinel** to call \`pauseBorrow\`, \`pauseSupply\`, and \`decreaseCF\` on EBrake - Grant **Guardian** and governance **Timelocks** the granular snapshot-reset permissions on EBrake - Grant the **per-chain 1-of-1 Multisig Pauser** the 8 IL-supported EBrake action functions for manual emergency pausing (Phase 0) - Whitelist Keeper + Guardian + 3 Timelocks as trusted keepers on DeviationSentinel -**Permission event summary**: 153 PermissionGranted (51 per chain × 3 chains), 0 PermissionRevoked +**Permission event summary**: 161 PermissionGranted (Ethereum 55 + Arbitrum One 51 + Base 55), 0 PermissionRevoked #### References diff --git a/vips/vip-667/bscmainnet.ts b/vips/vip-667/bscmainnet.ts index d41363337..9e34c5303 100644 --- a/vips/vip-667/bscmainnet.ts +++ b/vips/vip-667/bscmainnet.ts @@ -5,15 +5,62 @@ import { makeProposal } from "src/utils"; import { ARBITRUMONE_CONFIG } from "../vip-666/addresses/arbitrumone"; import { BASEMAINNET_CONFIG } from "../vip-666/addresses/basemainnet"; import { ETHEREUM_CONFIG } from "../vip-666/addresses/ethereum"; -import { ChainConfig, GOVERNANCE_EBRAKE_PERMS_IL, governanceAccounts, grant } from "../vip-666/bscmainnet"; +import { + ChainConfig, + GOVERNANCE_EBRAKE_PERMS_IL, + MonitoredMarket, + governanceAccounts, + grant, +} from "../vip-666/bscmainnet"; const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, BASEMAINNET_CONFIG]; +// Resolve the DEX oracle for a market based on its `oracleType`. Defaults to UniswapOracle. +// Misconfiguration (e.g. oracleType="curve" on a chain without a CurveOracle deployment) +// surfaces here at proposal-build time rather than at on-chain execution. +const resolveDexOracle = (cfg: ChainConfig, market: MonitoredMarket): string => { + switch (market.oracleType ?? "uniswap") { + case "uniswap": + return cfg.uniswapOracle; + case "curve": + if (!cfg.curveOracle) throw new Error(`${cfg.name}: ${market.symbol} requires curveOracle in ChainConfig`); + return cfg.curveOracle; + case "aerodrome": + if (!cfg.aerodromeOracle) + throw new Error(`${cfg.name}: ${market.symbol} requires aerodromeOracle in ChainConfig`); + return cfg.aerodromeOracle; + } +}; + +// Build the DEX-side `setPoolConfig` call for a market. UniswapOracle and AerodromeSlipstreamOracle +// share the (token, pool) signature; CurveOracle takes additional (coinIndex, referenceToken) +// fields tied to its StableSwap-NG `price_oracle` indexing scheme. +const buildSetPoolCommand = (cfg: ChainConfig, market: MonitoredMarket, dstChainId: number): Command => { + const oracle = resolveDexOracle(cfg, market); + if ((market.oracleType ?? "uniswap") === "curve") { + if (market.coinIndex === undefined || !market.referenceToken) { + throw new Error(`${cfg.name}: ${market.symbol} (curve) requires coinIndex + referenceToken`); + } + return { + target: oracle, + signature: "setPoolConfig(address,address,uint8,address)", + params: [market.token, market.pool, market.coinIndex, market.referenceToken], + dstChainId, + }; + } + return { + target: oracle, + signature: "setPoolConfig(address,address)", + params: [market.token, market.pool], + dstChainId, + }; +}; + // VIP-667 (Sub-B): governance EBrake action grants + per-market wiring. // Per-chain command count: gov ebrake action 32 + 3 × eligible markets. -// - Ethereum: 32 + 27 (9 mkts) = 59 -// - Arbitrum: 32 + 15 (5 mkts) = 47 -// - Base: 32 + 6 (2 mkts) = 38 +// - Ethereum: 32 + 30 (10 mkts: 9 Uniswap + 1 Curve) = 62 +// - Arbitrum: 32 + 15 ( 5 mkts: 5 Uniswap) = 47 +// - Base: 32 + 12 ( 4 mkts: 2 Uniswap + 2 Aero) = 44 // All under their respective block gas limits with the LayerZero adapter param. const buildChainCommandsB = (cfg: ChainConfig): Command[] => { const { acm, dstChainId } = cfg; @@ -26,22 +73,19 @@ const buildChainCommandsB = (cfg: ChainConfig): Command[] => { ]; // 2. Per-market wiring. For each eligible market, configure the DEX pool on the - // UniswapOracle, point the SentinelOracle at the UniswapOracle for that token, - // then enable deviation monitoring on the DeviationSentinel. Markets with a - // ZERO_ADDRESS token or pool are skipped (placeholder safety). + // chain-appropriate DEX oracle (Uniswap / Curve / Aerodrome Slipstream), point the + // SentinelOracle at that oracle for the token, then enable deviation monitoring on + // the DeviationSentinel. Markets with a ZERO_ADDRESS token or pool are skipped + // (placeholder safety). for (const market of cfg.monitoredMarkets) { if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; + const dexOracle = resolveDexOracle(cfg, market); commands.push( - { - target: cfg.uniswapOracle, - signature: "setPoolConfig(address,address)", - params: [market.token, market.pool], - dstChainId, - }, + buildSetPoolCommand(cfg, market, dstChainId), { target: cfg.sentinelOracle, signature: "setTokenOracleConfig(address,address)", - params: [market.token, cfg.uniswapOracle], + params: [market.token, dexOracle], dstChainId, }, { @@ -74,7 +118,7 @@ Because EBrake on these chains uses \`isIsolatedPool=true\`, only the IL-support If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: - Grant **Guardian** and governance **Timelocks** the 8 IL-supported EBrake action functions -- Configure deviation monitoring (10% threshold) for the eligible Core Pool markets on each chain — 9 on Ethereum, 5 on Arbitrum One, 2 on Base +- Configure deviation monitoring (10% threshold) for the eligible Core Pool markets on each chain — 10 on Ethereum (9 Uniswap V3 + 1 Curve / eBTC), 5 on Arbitrum One (Uniswap V3), 4 on Base (2 Uniswap V3 + 2 Aerodrome Slipstream / cbBTC + wstETH) **Permission event summary**: 96 PermissionGranted (32 per chain × 3 chains), 0 PermissionRevoked From 515ba6ec1781523af0d3ddfd993214bbf5aa7dda Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 30 Apr 2026 18:12:45 +0530 Subject: [PATCH 09/15] feat(vip-666/667): update CurveOracle to get_dy()-based pricing Replace price_oracle() EMA with get_dy() instantaneous swap output. New PoolConfig adds refCoinIndex and assetDecimals fields; setPoolConfig signature updated to (address,address,uint8,uint8,address,uint8). Redeploy proxy to 0x9F508F3 and bump Ethereum fork blocks to 24992834+. --- simulations/vip-666/abi/CurveOracle.json | 32 ++++++++++++++++++++++++ simulations/vip-666/ethereum.ts | 2 +- simulations/vip-667/ethereum.ts | 2 +- simulations/vip-667/shared.ts | 2 ++ vips/vip-666/addresses/ethereum.ts | 10 +++++--- vips/vip-666/bscmainnet.ts | 16 +++++++----- vips/vip-667/bscmainnet.ts | 26 ++++++++++++++----- 7 files changed, 73 insertions(+), 17 deletions(-) diff --git a/simulations/vip-666/abi/CurveOracle.json b/simulations/vip-666/abi/CurveOracle.json index dc913334b..00e518859 100644 --- a/simulations/vip-666/abi/CurveOracle.json +++ b/simulations/vip-666/abi/CurveOracle.json @@ -241,11 +241,23 @@ "name": "coinIndex", "type": "uint8" }, + { + "indexed": false, + "internalType": "uint8", + "name": "refCoinIndex", + "type": "uint8" + }, { "indexed": false, "internalType": "address", "name": "referenceToken", "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "assetDecimals", + "type": "uint8" } ], "name": "PoolConfigUpdated", @@ -362,10 +374,20 @@ "name": "coinIndex", "type": "uint8" }, + { + "internalType": "uint8", + "name": "refCoinIndex", + "type": "uint8" + }, { "internalType": "address", "name": "referenceToken", "type": "address" + }, + { + "internalType": "uint8", + "name": "assetDecimals", + "type": "uint8" } ], "stateMutability": "view", @@ -408,10 +430,20 @@ "name": "coinIndex", "type": "uint8" }, + { + "internalType": "uint8", + "name": "refCoinIndex", + "type": "uint8" + }, { "internalType": "address", "name": "referenceToken", "type": "address" + }, + { + "internalType": "uint8", + "name": "assetDecimals", + "type": "uint8" } ], "name": "setPoolConfig", diff --git a/simulations/vip-666/ethereum.ts b/simulations/vip-666/ethereum.ts index 2fa04139b..5b7950655 100644 --- a/simulations/vip-666/ethereum.ts +++ b/simulations/vip-666/ethereum.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; import { runVip666Suite } from "./shared"; -const FORK_BLOCK = 24985871; +const FORK_BLOCK = 24992853; forking(FORK_BLOCK, async () => { await runVip666Suite(ETHEREUM_CONFIG); diff --git a/simulations/vip-667/ethereum.ts b/simulations/vip-667/ethereum.ts index 07d4ac98e..5e0a82c39 100644 --- a/simulations/vip-667/ethereum.ts +++ b/simulations/vip-667/ethereum.ts @@ -4,7 +4,7 @@ import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; import { runVip667Suite } from "./shared"; // Must be ≥ 24985630 — CurveOracle proxy deployment block. -const FORK_BLOCK = 24985871; +const FORK_BLOCK = 24992834; forking(FORK_BLOCK, async () => { await runVip667Suite(ETHEREUM_CONFIG); diff --git a/simulations/vip-667/shared.ts b/simulations/vip-667/shared.ts index 863be6d16..5eb9a456d 100644 --- a/simulations/vip-667/shared.ts +++ b/simulations/vip-667/shared.ts @@ -251,9 +251,11 @@ export const runVip667Suite = async (cfg: ChainConfig) => { const cfgEntry = await curveOracle!.poolConfigs(market.token); expect(ethers.utils.getAddress(cfgEntry.pool)).to.equal(ethers.utils.getAddress(market.pool)); expect(cfgEntry.coinIndex).to.equal(market.coinIndex); + expect(cfgEntry.refCoinIndex).to.equal(market.refCoinIndex); expect(ethers.utils.getAddress(cfgEntry.referenceToken)).to.equal( ethers.utils.getAddress(market.referenceToken as string), ); + expect(cfgEntry.assetDecimals).to.equal(market.assetDecimals); } else if (oracleType === "aerodrome") { const actual = await aerodromeOracle!.tokenPools(market.token); expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); diff --git a/vips/vip-666/addresses/ethereum.ts b/vips/vip-666/addresses/ethereum.ts index 0328a3cf8..825041408 100644 --- a/vips/vip-666/addresses/ethereum.ts +++ b/vips/vip-666/addresses/ethereum.ts @@ -18,7 +18,7 @@ export const ETHEREUM_SENTINEL_ORACLE = "0x444C53E194B40c272fAd683210e2cB1c16Ab1 export const ETHEREUM_UNISWAP_ORACLE = "0x873993F8f5f5Ddbae0952e939ab3005Af363Af00"; // Deployed via venus-periphery PR #66 — prices Curve StableSwap-NG assets that // UniswapOracle can't read (eBTC/WBTC pool, etc.). -export const ETHEREUM_CURVE_ORACLE = "0x64a811bd0E91cf00D9CE0769eDA028026577A6D9"; +export const ETHEREUM_CURVE_ORACLE = "0x9F508F3146cb03276282f9237c6eE64f76E3261D"; export const ETHEREUM_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig export const ETHEREUM_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; @@ -66,8 +66,9 @@ export const ETHEREUM_MONITORED_MARKETS = [ pool: "0xe6d7ebb9f1a9519dc06d557e03c522d53520e76a", // USDe/USDC Uniswap V3 deviationPercent: 10, }, - // eBTC routes through CurveOracle (eBTC/WBTC StableSwap-NG pool). coinIndex=0 because - // eBTC is coins[0] on this pool; referenceToken is the paired token (WBTC). + // eBTC routes through CurveOracle (eBTC/WBTC StableSwap-NG pool). + // coinIndex=0 (eBTC is coins[0]), refCoinIndex=1 (WBTC is coins[1]). + // assetDecimals=8 matches eBTC's ERC-20 decimals; get_dy probes 10^8 units. { symbol: "eBTC", token: "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", @@ -75,7 +76,9 @@ export const ETHEREUM_MONITORED_MARKETS = [ deviationPercent: 10, oracleType: "curve" as const, coinIndex: 0, + refCoinIndex: 1, referenceToken: WBTC, + assetDecimals: 8, }, { symbol: "DAI", @@ -111,6 +114,7 @@ export const ETHEREUM_CONFIG = { sentinelOracle: ETHEREUM_SENTINEL_ORACLE, uniswapOracle: ETHEREUM_UNISWAP_ORACLE, curveOracle: ETHEREUM_CURVE_ORACLE, + multisigPauser: ETHEREUM_MULTISIG_PAUSER, keeper: ETHEREUM_KEEPER, monitoredMarkets: ETHEREUM_MONITORED_MARKETS, diff --git a/vips/vip-666/bscmainnet.ts b/vips/vip-666/bscmainnet.ts index 4379ea84a..b7e8296eb 100644 --- a/vips/vip-666/bscmainnet.ts +++ b/vips/vip-666/bscmainnet.ts @@ -13,13 +13,17 @@ export interface MonitoredMarket { pool: string; deviationPercent: number; // Defaults to "uniswap" for the existing UniswapOracle path. "curve" routes through - // CurveOracle and requires coinIndex + referenceToken; "aerodrome" routes through - // AerodromeSlipstreamOracle and uses the Uniswap-shaped (token, pool) signature. + // CurveOracle and requires coinIndex + refCoinIndex + referenceToken + assetDecimals; + // "aerodrome" routes through AerodromeSlipstreamOracle and uses the Uniswap-shaped (token, pool) signature. oracleType?: OracleType; - // Curve-only: index of the asset in the StableSwap-NG pool's coins() array. + // Curve-only: coins() index of the priced asset in the StableSwap-NG pool. coinIndex?: number; - // Curve-only: the paired token whose USD price is fetched from ResilientOracle. + // Curve-only: coins() index of the reference asset (whose USD price ResilientOracle supplies). + refCoinIndex?: number; + // Curve-only: address of the reference asset. referenceToken?: string; + // Curve-only: decimals of the priced asset (used to scale get_dy() output). + assetDecimals?: number; } export interface ChainConfig { @@ -67,8 +71,8 @@ export const SENTINEL_ORACLE_ADMIN_PERMS = ["setTokenOracleConfig(address,addres export const UNISWAP_ORACLE_ADMIN_PERMS = ["setPoolConfig(address,address)"]; // CurveOracle access-controlled functions — distinct setPoolConfig signature -// (StableSwap-NG needs coinIndex + referenceToken in addition to token + pool). -export const CURVE_ORACLE_ADMIN_PERMS = ["setPoolConfig(address,address,uint8,address)"]; +// (StableSwap-NG needs coinIndex + refCoinIndex + referenceToken + assetDecimals in addition to token + pool). +export const CURVE_ORACLE_ADMIN_PERMS = ["setPoolConfig(address,address,uint8,uint8,address,uint8)"]; // AerodromeSlipstreamOracle access-controlled functions — same shape as UniswapOracle. export const AERODROME_ORACLE_ADMIN_PERMS = ["setPoolConfig(address,address)"]; diff --git a/vips/vip-667/bscmainnet.ts b/vips/vip-667/bscmainnet.ts index 9e34c5303..aea1078bc 100644 --- a/vips/vip-667/bscmainnet.ts +++ b/vips/vip-667/bscmainnet.ts @@ -33,18 +33,32 @@ const resolveDexOracle = (cfg: ChainConfig, market: MonitoredMarket): string => }; // Build the DEX-side `setPoolConfig` call for a market. UniswapOracle and AerodromeSlipstreamOracle -// share the (token, pool) signature; CurveOracle takes additional (coinIndex, referenceToken) -// fields tied to its StableSwap-NG `price_oracle` indexing scheme. +// share the (token, pool) signature; CurveOracle takes additional (coinIndex, refCoinIndex, +// referenceToken, assetDecimals) fields for its get_dy()-based pricing scheme. const buildSetPoolCommand = (cfg: ChainConfig, market: MonitoredMarket, dstChainId: number): Command => { const oracle = resolveDexOracle(cfg, market); if ((market.oracleType ?? "uniswap") === "curve") { - if (market.coinIndex === undefined || !market.referenceToken) { - throw new Error(`${cfg.name}: ${market.symbol} (curve) requires coinIndex + referenceToken`); + if ( + market.coinIndex === undefined || + market.refCoinIndex === undefined || + !market.referenceToken || + market.assetDecimals === undefined + ) { + throw new Error( + `${cfg.name}: ${market.symbol} (curve) requires coinIndex + refCoinIndex + referenceToken + assetDecimals`, + ); } return { target: oracle, - signature: "setPoolConfig(address,address,uint8,address)", - params: [market.token, market.pool, market.coinIndex, market.referenceToken], + signature: "setPoolConfig(address,address,uint8,uint8,address,uint8)", + params: [ + market.token, + market.pool, + market.coinIndex, + market.refCoinIndex, + market.referenceToken, + market.assetDecimals, + ], dstChainId, }; } From 4a2afc076817affe27fd779a95ee05b2378bfeb7 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 30 Apr 2026 19:58:47 +0530 Subject: [PATCH 10/15] feat(vip-666): add Sepolia testnet bootstrap for DeviationSentinel + EBrake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Minimal 22-command VIP (bsctestnet.ts) targeting Sepolia via LZ — fits within the testnet relayer's ~10 KB payload ceiling; the full governance admin set is deferred to mainnet VIPs - Deploys ownership, EBrake→Comptroller and Sentinel→EBrake wiring, trusted keeper whitelist (deployer EOA + on-chain keeper), deployer extra permissions for the E2E reset cycle, and market config for WETH + WBTC at 10% threshold - Sepolia address config (addresses/sepolia.ts) with deployed contract addresses and keeper set to match the BSC testnet keeper address - Fork simulation (simulations/vip-666/sepolia.ts) covering pre/post VIP state: ownership, permissions, trusted keepers, market config - Adds WETH address to src/networkAddresses.ts sepolia entry --- simulations/vip-666/sepolia.ts | 269 ++++++++++++++++++++++++++++++ src/networkAddresses.ts | 2 + vips/vip-666/addresses/sepolia.ts | 67 ++++++++ vips/vip-666/bsctestnet.ts | 109 ++++++++++++ 4 files changed, 447 insertions(+) create mode 100644 simulations/vip-666/sepolia.ts create mode 100644 vips/vip-666/addresses/sepolia.ts create mode 100644 vips/vip-666/bsctestnet.ts diff --git a/simulations/vip-666/sepolia.ts b/simulations/vip-666/sepolia.ts new file mode 100644 index 000000000..957aee47a --- /dev/null +++ b/simulations/vip-666/sepolia.ts @@ -0,0 +1,269 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { ZERO_ADDRESS } from "src/networkAddresses"; +import { expectEvents, initMainnetUser } from "src/utils"; +import { forking, testForkedNetworkVipCommands } from "src/vip-framework"; + +import { SEPOLIA_CONFIG, SEPOLIA_GUARDIAN_OWNER } from "../../vips/vip-666/addresses/sepolia"; +import { + DIAMOND_ONLY_EBRAKE_PERMS, + EBRAKE_COMPTROLLER_PERMS_IL, + SENTINEL_EBRAKE_PERMS, +} from "../../vips/vip-666/bscmainnet"; +import vip666Sepolia, { + DEPLOYER_COMPTROLLER_PERMS, + DEPLOYER_EBRAKE_PERMS, + DEPLOYER_SENTINEL_ORACLE_PERMS, +} from "../../vips/vip-666/bsctestnet"; +import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; +import DEVIATION_SENTINEL_ABI from "./abi/DeviationSentinel.json"; +import EBRAKE_ABI from "./abi/EBrake.json"; +import SENTINEL_ORACLE_ABI from "./abi/SentinelOracle.json"; + +const FORK_BLOCK = 10761588; + +const cfg = SEPOLIA_CONFIG; + +// Trusted keepers in the new minimal VIP: +// - SEPOLIA_GUARDIAN_OWNER: deployer EOA, drives the manual E2E test cycle +// - cfg.keeper (= cfg.guardian on Sepolia): on-chain keeper backup +// These are two distinct addresses (GUARDIAN_OWNER ≠ GUARDIAN). +const TRUSTED_KEEPERS = [SEPOLIA_GUARDIAN_OWNER, cfg.keeper]; + +// RoleGranted events emitted by the unified 22-command VIP: +// normalTimelock setTrustedKeeper perm on DS: 1 +// normalTimelock setTokenConfig perm on DS: 1 +// EBrake → Comptroller (4 sigs): 4 +// Sentinel → EBrake (3 sigs): 3 +// Deployer extra (1 + 3 + 2): 6 +// --- +// 15 +const EXPECTED_ROLE_GRANTED = 15; + +forking(FORK_BLOCK, async () => { + let acm: Contract; + let deviationSentinel: Contract; + let eBrake: Contract; + let sentinelOracle: Contract; + + let impersonatedDeviationSentinel: SignerWithAddress; + let impersonatedSentinelOracle: SignerWithAddress; + let impersonatedComptroller: SignerWithAddress; + + before(async () => { + acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, cfg.acm); + deviationSentinel = await ethers.getContractAt(DEVIATION_SENTINEL_ABI, cfg.deviationSentinel); + eBrake = await ethers.getContractAt(EBRAKE_ABI, cfg.eBrake); + sentinelOracle = await ethers.getContractAt(SENTINEL_ORACLE_ABI, cfg.sentinelOracle); + + impersonatedDeviationSentinel = await initMainnetUser(cfg.deviationSentinel, ethers.utils.parseEther("1")); + impersonatedSentinelOracle = await initMainnetUser(cfg.sentinelOracle, ethers.utils.parseEther("1")); + impersonatedComptroller = await initMainnetUser(cfg.comptroller, ethers.utils.parseEther("1")); + }); + + describe("VIP-666 [Sepolia] — Pre-VIP behaviour", () => { + it("DeviationSentinel pendingOwner is Normal Timelock", async () => { + expect(await deviationSentinel.pendingOwner()).to.equal(cfg.normalTimelock); + }); + + it("SentinelOracle pendingOwner is Normal Timelock", async () => { + expect(await sentinelOracle.pendingOwner()).to.equal(cfg.normalTimelock); + }); + + it("EBrake pendingOwner is Normal Timelock", async () => { + expect(await eBrake.pendingOwner()).to.equal(cfg.normalTimelock); + }); + + it("EBrake immutables are correctly set", async () => { + expect(await eBrake.COMPTROLLER()).to.equal(cfg.comptroller); + expect(await eBrake.IS_ISOLATED_POOL()).to.equal(true); + }); + + it("DeviationSentinel immutables wire to local EBrake + SentinelOracle", async () => { + expect(await deviationSentinel.EBRAKE()).to.equal(cfg.eBrake); + expect(await deviationSentinel.SENTINEL_ORACLE()).to.equal(cfg.sentinelOracle); + }); + + // Use hasPermission (not isAllowedToCall) to avoid false positives from wildcard + // ACM grants already present on Sepolia testnet. + it("EBrake has no permissions on the Comptroller yet", async () => { + for (const sig of EBRAKE_COMPTROLLER_PERMS_IL) { + expect(await acm.hasPermission(cfg.eBrake, cfg.comptroller, sig)).to.equal(false, `unexpected: ${sig}`); + } + }); + + it("DeviationSentinel has no action permissions on EBrake yet", async () => { + for (const sig of SENTINEL_EBRAKE_PERMS) { + expect(await acm.hasPermission(cfg.deviationSentinel, cfg.eBrake, sig)).to.equal(false, `unexpected: ${sig}`); + } + }); + + it("No accounts are whitelisted as trusted keepers yet", async () => { + for (const account of TRUSTED_KEEPERS) { + expect(await deviationSentinel.trustedKeepers(account)).to.equal(false); + } + }); + + it("Deployer EOA has no extra permissions yet", async () => { + for (const sig of DEPLOYER_SENTINEL_ORACLE_PERMS) { + expect(await acm.hasPermission(SEPOLIA_GUARDIAN_OWNER, cfg.sentinelOracle, sig)).to.equal(false); + } + for (const sig of DEPLOYER_EBRAKE_PERMS) { + expect(await acm.hasPermission(SEPOLIA_GUARDIAN_OWNER, cfg.eBrake, sig)).to.equal(false); + } + for (const sig of DEPLOYER_COMPTROLLER_PERMS) { + expect(await acm.hasPermission(SEPOLIA_GUARDIAN_OWNER, cfg.comptroller, sig)).to.equal(false); + } + }); + + it("WETH monitoring is not configured yet", async () => { + const tc = await deviationSentinel.tokenConfigs(cfg.monitoredMarkets[0].token); + expect(tc.deviation).to.equal(0); + expect(tc.enabled).to.equal(false); + }); + + it("WBTC monitoring is not configured yet", async () => { + const tc = await deviationSentinel.tokenConfigs(cfg.monitoredMarkets[1].token); + expect(tc.deviation).to.equal(0); + expect(tc.enabled).to.equal(false); + }); + }); + + testForkedNetworkVipCommands("VIP-666 [Sepolia] Minimal E2E Bootstrap", await vip666Sepolia(), { + callbackAfterExecution: async txResponse => { + // acceptOwnership(): DeviationSentinel + SentinelOracle + EBrake. + // All three share the same OwnershipTransferred topic so any ABI catches all 3. + await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["OwnershipTransferred"], [3]); + + // setTrustedKeeper: 2 accounts (GUARDIAN_OWNER + keeper/guardian) + await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TrustedKeeperUpdated"], [TRUSTED_KEEPERS.length]); + + // setTokenConfig: WETH + WBTC + await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TokenConfigUpdated"], [cfg.monitoredMarkets.length]); + + // RoleGranted: see EXPECTED_ROLE_GRANTED comment above + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [EXPECTED_ROLE_GRANTED]); + }, + }); + + describe("VIP-666 [Sepolia] — Post-VIP behaviour", () => { + describe("Ownership", () => { + it("DeviationSentinel owner is Normal Timelock", async () => { + expect(await deviationSentinel.owner()).to.equal(cfg.normalTimelock); + expect(await deviationSentinel.pendingOwner()).to.equal(ZERO_ADDRESS); + }); + + it("SentinelOracle owner is Normal Timelock", async () => { + expect(await sentinelOracle.owner()).to.equal(cfg.normalTimelock); + expect(await sentinelOracle.pendingOwner()).to.equal(ZERO_ADDRESS); + }); + + it("EBrake owner is Normal Timelock", async () => { + expect(await eBrake.owner()).to.equal(cfg.normalTimelock); + expect(await eBrake.pendingOwner()).to.equal(ZERO_ADDRESS); + }); + }); + + describe("Permissions — EBrake → Comptroller", () => { + it("EBrake has all four IL-supported permissions on the Comptroller", async () => { + const a = acm.connect(impersonatedComptroller); + for (const sig of EBRAKE_COMPTROLLER_PERMS_IL) { + expect(await a.isAllowedToCall(cfg.eBrake, sig)).to.equal(true, `missing: ${sig}`); + } + }); + }); + + describe("Permissions — Sentinel → EBrake", () => { + it("DeviationSentinel has the three handleDeviation permissions on EBrake", async () => { + for (const sig of SENTINEL_EBRAKE_PERMS) { + expect(await acm.hasPermission(cfg.deviationSentinel, cfg.eBrake, sig)).to.equal(true, `missing: ${sig}`); + } + }); + }); + + describe("Permissions — Diamond-only NOT granted", () => { + it("Diamond-only EBrake permissions are not granted", async () => { + for (const sig of DIAMOND_ONLY_EBRAKE_PERMS) { + expect(await acm.hasPermission(cfg.deviationSentinel, cfg.eBrake, sig)).to.equal( + false, + `unexpected Diamond-only: ${sig}`, + ); + } + }); + }); + + describe("Trusted keepers", () => { + it("Deployer EOA and on-chain keeper are whitelisted", async () => { + for (const account of TRUSTED_KEEPERS) { + expect(await deviationSentinel.trustedKeepers(account)).to.equal(true, `not whitelisted: ${account}`); + } + }); + }); + + describe("Deployer EOA extra permissions", () => { + it("Deployer EOA has setDirectPrice on SentinelOracle", async () => { + const a = acm.connect(impersonatedSentinelOracle); + for (const sig of DEPLOYER_SENTINEL_ORACLE_PERMS) { + expect(await a.isAllowedToCall(SEPOLIA_GUARDIAN_OWNER, sig)).to.equal(true, `missing: ${sig}`); + } + }); + + it("Deployer EOA has reset perms on EBrake", async () => { + for (const sig of DEPLOYER_EBRAKE_PERMS) { + expect(await acm.hasPermission(SEPOLIA_GUARDIAN_OWNER, cfg.eBrake, sig)).to.equal(true, `missing: ${sig}`); + } + }); + + it("Deployer EOA has setCollateralFactor + setActionsPaused on Comptroller", async () => { + for (const sig of DEPLOYER_COMPTROLLER_PERMS) { + expect(await acm.hasPermission(SEPOLIA_GUARDIAN_OWNER, cfg.comptroller, sig)).to.equal( + true, + `missing: ${sig}`, + ); + } + }); + }); + + describe("Permissions — Normal Timelock can call DS functions used in this VIP", () => { + it("Normal Timelock has setTrustedKeeper permission on DeviationSentinel", async () => { + const a = acm.connect(impersonatedDeviationSentinel); + expect(await a.isAllowedToCall(cfg.normalTimelock, "setTrustedKeeper(address,bool)")).to.equal(true); + }); + + it("Normal Timelock has setTokenConfig permission on DeviationSentinel", async () => { + const a = acm.connect(impersonatedDeviationSentinel); + expect(await a.isAllowedToCall(cfg.normalTimelock, "setTokenConfig(address,(uint8,bool))")).to.equal(true); + }); + }); + + describe("Market monitoring config", () => { + it("WETH monitoring is enabled with 10% threshold", async () => { + const market = cfg.monitoredMarkets[0]; + expect(market.symbol).to.equal("WETH"); + const tc = await deviationSentinel.tokenConfigs(market.token); + expect(tc.deviation).to.equal(market.deviationPercent); + expect(tc.enabled).to.equal(true); + }); + + it("WBTC monitoring is enabled with 10% threshold", async () => { + const market = cfg.monitoredMarkets[1]; + expect(market.symbol).to.equal("WBTC"); + const tc = await deviationSentinel.tokenConfigs(market.token); + expect(tc.deviation).to.equal(market.deviationPercent); + expect(tc.enabled).to.equal(true); + }); + + it("SentinelOracle has no DEX oracle wired for WETH (direct price flow)", async () => { + const tc = await sentinelOracle.tokenConfigs(cfg.monitoredMarkets[0].token); + expect(tc.oracle ?? tc).to.equal(ZERO_ADDRESS); + }); + + it("SentinelOracle has no DEX oracle wired for WBTC (direct price flow)", async () => { + const tc = await sentinelOracle.tokenConfigs(cfg.monitoredMarkets[1].token); + expect(tc.oracle ?? tc).to.equal(ZERO_ADDRESS); + }); + }); + }); +}); diff --git a/src/networkAddresses.ts b/src/networkAddresses.ts index 68e8685e9..c985fd67f 100644 --- a/src/networkAddresses.ts +++ b/src/networkAddresses.ts @@ -102,6 +102,7 @@ export const NETWORK_ADDRESSES = { FAST_TRACK_TIMELOCK: "0x7F043F43Adb392072a3Ba0cC9c96e894C6f7e182", CRITICAL_TIMELOCK: "0xA24A7A65b8968a749841988Bd7d05F6a94329fDe", GUARDIAN: "0x94fa6078b6b8a26F0B6EDFFBE6501B22A10470fB", + ACCESS_CONTROL_MANAGER: "0xbf705C00578d43B6147ab4eaE04DBBEd1ccCdc96", CHAINLINK_ORACLE: oracleSepoliaContracts.addresses.ChainlinkOracle, RESILIENT_ORACLE: oracleSepoliaContracts.addresses.ResilientOracle, REDSTONE_ORACLE: oracleSepoliaContracts.addresses.RedStoneOracle, @@ -114,6 +115,7 @@ export const NETWORK_ADDRESSES = { ENDPOINT: "0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1", LZ_LIBRARY: "0x3acaaf60502791d199a5a5f0b173d78229ebfe32", OMNICHAIN_GOVERNANCE_EXECUTOR: "0xD9B18a43Ee9964061c1A1925Aa907462F0249109", + CORE_COMPTROLLER: "0x7Aa39ab4BcA897F403425C9C6FDbd0f882Be0D70", }, opbnbtestnet: { NORMAL_TIMELOCK: "0x1c4e015Bd435Efcf4f58D82B0d0fBa8fC4F81120", diff --git a/vips/vip-666/addresses/sepolia.ts b/vips/vip-666/addresses/sepolia.ts new file mode 100644 index 000000000..9fc817124 --- /dev/null +++ b/vips/vip-666/addresses/sepolia.ts @@ -0,0 +1,67 @@ +import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { LzChainId } from "src/types"; + +const { sepolia } = NETWORK_ADDRESSES; + +// Already-deployed governance + protocol addresses on Sepolia +export const SEPOLIA_GUARDIAN_OWNER = "0xFEA1c651A47FE29dB9b1bf3cC1f224d8D9CFF68C"; // deployer EOA +export const SEPOLIA_ACM = sepolia.ACCESS_CONTROL_MANAGER; +export const SEPOLIA_GUARDIAN = sepolia.GUARDIAN; +export const SEPOLIA_NORMAL_TIMELOCK = sepolia.NORMAL_TIMELOCK; +export const SEPOLIA_FAST_TRACK_TIMELOCK = sepolia.FAST_TRACK_TIMELOCK; +export const SEPOLIA_CRITICAL_TIMELOCK = sepolia.CRITICAL_TIMELOCK; +export const SEPOLIA_CORE_COMPTROLLER = sepolia.CORE_COMPTROLLER; + +// Deployed via venus-periphery feat/VPD-1134 +export const SEPOLIA_DEVIATION_SENTINEL = "0x3a22AA95998a6c5f57e86E24fEA1503452Bdfa39"; +export const SEPOLIA_EBRAKE = "0x44784FBa07b5199a7a21C8A8E4a50c45137227BC"; +export const SEPOLIA_SENTINEL_ORACLE = "0xb5A87b6738C1cB2f1a1Acae8c49DbE32f8034CA5"; +// UniswapOracle not deployed on testnet — deviation testing uses SentinelOracle.setDirectPrice() +// instead of live DEX feeds. Ownership acceptance is skipped for ZERO_ADDRESS targets in the +// Sepolia VIP builder. +export const SEPOLIA_UNISWAP_ORACLE = ZERO_ADDRESS; + +// On Sepolia testnet the guardian EOA acts as both keeper and multisig pauser so that +// all emergency actions can be triggered manually after the VIP executes once. +export const SEPOLIA_MULTISIG_PAUSER = sepolia.GUARDIAN; +export const SEPOLIA_KEEPER = "0x2Ce1d0ffD7E869D9DF33e28552b12DdDed326706"; // same keeper as BSC testnet + +export const SEPOLIA_DST_CHAIN_ID = LzChainId.sepolia; + +// Underlying token addresses for the two monitored markets. +// Pools are ZERO_ADDRESS — testnet deviation is simulated via SentinelOracle.setDirectPrice(). +// The VIP-667 builder skips setPoolConfig / setTokenOracleConfig for markets with a zero pool. +const WBTC = "0x92A2928f5634BEa89A195e7BeCF0f0FEEDAB885b"; // MockWBTC on Sepolia + +export const SEPOLIA_MONITORED_MARKETS = [ + { + symbol: "WETH", + token: sepolia.WETH, + pool: ZERO_ADDRESS, + deviationPercent: 10, + }, + { + symbol: "WBTC", + token: WBTC, + pool: ZERO_ADDRESS, + deviationPercent: 10, + }, +]; + +export const SEPOLIA_CONFIG = { + name: "Sepolia", + dstChainId: SEPOLIA_DST_CHAIN_ID, + acm: SEPOLIA_ACM, + guardian: SEPOLIA_GUARDIAN, + normalTimelock: SEPOLIA_NORMAL_TIMELOCK, + fastTrackTimelock: SEPOLIA_FAST_TRACK_TIMELOCK, + criticalTimelock: SEPOLIA_CRITICAL_TIMELOCK, + comptroller: SEPOLIA_CORE_COMPTROLLER, + deviationSentinel: SEPOLIA_DEVIATION_SENTINEL, + eBrake: SEPOLIA_EBRAKE, + sentinelOracle: SEPOLIA_SENTINEL_ORACLE, + uniswapOracle: SEPOLIA_UNISWAP_ORACLE, + multisigPauser: SEPOLIA_MULTISIG_PAUSER, + keeper: SEPOLIA_KEEPER, + monitoredMarkets: SEPOLIA_MONITORED_MARKETS, +} as const; diff --git a/vips/vip-666/bsctestnet.ts b/vips/vip-666/bsctestnet.ts new file mode 100644 index 000000000..d715d1413 --- /dev/null +++ b/vips/vip-666/bsctestnet.ts @@ -0,0 +1,109 @@ +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { SEPOLIA_CONFIG, SEPOLIA_GUARDIAN_OWNER } from "./addresses/sepolia"; +import { EBRAKE_COMPTROLLER_PERMS_IL, SENTINEL_EBRAKE_PERMS, grant } from "./bscmainnet"; + +// Exported for simulation imports. +export const DEPLOYER_SENTINEL_ORACLE_PERMS = ["setDirectPrice(address,uint256)"]; +export const DEPLOYER_EBRAKE_PERMS = [ + "resetCFSnapshot(address)", + "resetBorrowCapSnapshot(address)", + "resetSupplyCapSnapshot(address)", +]; +export const DEPLOYER_COMPTROLLER_PERMS = [ + "setCollateralFactor(address,uint256,uint256)", + "setActionsPaused(address[],uint256[],bool)", +]; + +// Minimal single-VIP Sepolia testnet bootstrap for DeviationSentinel + EBrakeV2 E2E testing. +// +// Command count: 3 (ownership) + 2 (normalTimelock DS perms) + 4 (EBrake→Comptroller) +// + 3 (Sentinel→EBrake) + 2 (trusted keepers) + 6 (deployer extra) + 2 (market config) = 22 +// +// 22 × ~411 bytes ≈ 9.2 KB — under the LZ V1 testnet relayer 10 KB payload ceiling. +// +// The broader governance permission set (all 4 gov accounts × every sig) from the mainnet +// split is intentionally omitted: the LZ testnet relayer rejects payloads > ~10 KB, and +// those admin grants are not needed to drive the E2E deviation → pause → reset flow. +export const vip666Sepolia = () => { + const cfg = SEPOLIA_CONFIG; + const { acm, dstChainId } = cfg; + + const meta = { + version: "v2", + title: "VIP-666 [Sepolia] DeviationSentinel + EBrakeV2 — Minimal E2E Bootstrap", + description: `#### Summary + +One-time Sepolia testnet VIP to wire the **DeviationSentinel** + **EBrakeV2** stack for E2E deviation testing. + +Since Sepolia has no live DEX feeds, price deviation is simulated via \`SentinelOracle.setDirectPrice()\`. Only the permissions strictly needed for the E2E test cycle are granted; the full governance admin set from the mainnet VIPs is omitted to stay within the LZ testnet relayer payload limit (~10 KB). + +**E2E test cycle after this VIP:** +1. Deployer calls \`SentinelOracle.setDirectPrice(token, artificialPrice)\` +2. Deployer calls \`DeviationSentinel.handleDeviation(vToken)\` as trusted keeper → EBrake pauses the market +3. Deployer calls \`EBrake.resetCFSnapshot / resetBorrowCapSnapshot / resetSupplyCapSnapshot\` +4. Deployer calls \`Comptroller.setCollateralFactor\` / \`setActionsPaused\` to restore state`, + forDescription: "Execute this proposal", + againstDescription: "Do not execute this proposal", + abstainDescription: "Indifferent to execution", + }; + + return makeProposal( + [ + // 1. Accept ownership of the three deployed contracts. + // UniswapOracle is ZERO_ADDRESS on Sepolia — skip. + { target: cfg.deviationSentinel, signature: "acceptOwnership()", params: [], dstChainId }, + { target: cfg.sentinelOracle, signature: "acceptOwnership()", params: [], dstChainId }, + { target: cfg.eBrake, signature: "acceptOwnership()", params: [], dstChainId }, + + // 2. Grant Normal Timelock the two DeviationSentinel functions it calls later in this VIP. + // These grants must precede the setTrustedKeeper and setTokenConfig calls below. + grant(acm, cfg.deviationSentinel, "setTrustedKeeper(address,bool)", cfg.normalTimelock, dstChainId), + grant(acm, cfg.deviationSentinel, "setTokenConfig(address,(uint8,bool))", cfg.normalTimelock, dstChainId), + + // 3. Grant EBrake the IL-supported emergency-action permissions on the Comptroller. (4) + ...EBRAKE_COMPTROLLER_PERMS_IL.map(sig => grant(acm, cfg.comptroller, sig, cfg.eBrake, dstChainId)), + + // 4. Grant DeviationSentinel the three EBrake functions handleDeviation invokes. (3) + ...SENTINEL_EBRAKE_PERMS.map(sig => grant(acm, cfg.eBrake, sig, cfg.deviationSentinel, dstChainId)), + + // 5. Whitelist trusted keepers. (2) + // - SEPOLIA_GUARDIAN_OWNER (deployer EOA): drives the manual E2E test cycle + // - cfg.keeper (= on-chain Guardian): standard keeper backup + { + target: cfg.deviationSentinel, + signature: "setTrustedKeeper(address,bool)", + params: [SEPOLIA_GUARDIAN_OWNER, true], + dstChainId, + }, + { + target: cfg.deviationSentinel, + signature: "setTrustedKeeper(address,bool)", + params: [cfg.keeper, true], + dstChainId, + }, + + // 6. Grant deployer EOA the extra permissions needed to drive the full E2E test cycle. (6) + ...DEPLOYER_SENTINEL_ORACLE_PERMS.map(sig => + grant(acm, cfg.sentinelOracle, sig, SEPOLIA_GUARDIAN_OWNER, dstChainId), + ), + ...DEPLOYER_EBRAKE_PERMS.map(sig => grant(acm, cfg.eBrake, sig, SEPOLIA_GUARDIAN_OWNER, dstChainId)), + ...DEPLOYER_COMPTROLLER_PERMS.map(sig => grant(acm, cfg.comptroller, sig, SEPOLIA_GUARDIAN_OWNER, dstChainId)), + + // 7. Enable deviation monitoring (10% threshold) for WETH and WBTC. (2) + // setPoolConfig / setTokenOracleConfig are skipped — no live DEX pools on Sepolia; + // prices are injected manually via SentinelOracle.setDirectPrice(). + ...cfg.monitoredMarkets.map(market => ({ + target: cfg.deviationSentinel, + signature: "setTokenConfig(address,(uint8,bool))", + params: [market.token, [market.deviationPercent, true]], + dstChainId, + })), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip666Sepolia; From d21f30ea642f871e05cd8ec51e11f98cf6ee18d4 Mon Sep 17 00:00:00 2001 From: Fred Date: Sun, 3 May 2026 09:13:18 +0000 Subject: [PATCH 11/15] chore: rename vip-666/667 to vip-616/617 for proposal --- simulations/{vip-666 => vip-616}/abi/AccessControlManager.json | 0 .../{vip-666 => vip-616}/abi/AerodromeSlipstreamOracle.json | 0 simulations/{vip-666 => vip-616}/abi/CurveOracle.json | 0 simulations/{vip-666 => vip-616}/abi/DeviationSentinel.json | 0 simulations/{vip-666 => vip-616}/abi/EBrake.json | 0 simulations/{vip-666 => vip-616}/abi/ILComptroller.json | 0 simulations/{vip-666 => vip-616}/abi/ResilientOracle.json | 0 simulations/{vip-666 => vip-616}/abi/SentinelOracle.json | 0 simulations/{vip-666 => vip-616}/abi/UniswapOracle.json | 0 simulations/{vip-666 => vip-616}/abi/VToken.json | 0 simulations/{vip-666 => vip-616}/arbitrumone.ts | 0 simulations/{vip-666 => vip-616}/basemainnet.ts | 0 simulations/{vip-666 => vip-616}/ethereum.ts | 0 simulations/{vip-666 => vip-616}/sepolia.ts | 0 simulations/{vip-666 => vip-616}/shared.ts | 0 simulations/{vip-667 => vip-617}/arbitrumone.ts | 0 simulations/{vip-667 => vip-617}/basemainnet.ts | 0 simulations/{vip-667 => vip-617}/ethereum.ts | 0 simulations/{vip-667 => vip-617}/shared.ts | 0 vips/{vip-666 => vip-616}/addresses/arbitrumone.ts | 0 vips/{vip-666 => vip-616}/addresses/basemainnet.ts | 0 vips/{vip-666 => vip-616}/addresses/ethereum.ts | 0 vips/{vip-666 => vip-616}/addresses/sepolia.ts | 0 vips/{vip-666 => vip-616}/bscmainnet.ts | 0 vips/{vip-666 => vip-616}/bsctestnet.ts | 0 vips/{vip-667 => vip-617}/bscmainnet.ts | 0 26 files changed, 0 insertions(+), 0 deletions(-) rename simulations/{vip-666 => vip-616}/abi/AccessControlManager.json (100%) rename simulations/{vip-666 => vip-616}/abi/AerodromeSlipstreamOracle.json (100%) rename simulations/{vip-666 => vip-616}/abi/CurveOracle.json (100%) rename simulations/{vip-666 => vip-616}/abi/DeviationSentinel.json (100%) rename simulations/{vip-666 => vip-616}/abi/EBrake.json (100%) rename simulations/{vip-666 => vip-616}/abi/ILComptroller.json (100%) rename simulations/{vip-666 => vip-616}/abi/ResilientOracle.json (100%) rename simulations/{vip-666 => vip-616}/abi/SentinelOracle.json (100%) rename simulations/{vip-666 => vip-616}/abi/UniswapOracle.json (100%) rename simulations/{vip-666 => vip-616}/abi/VToken.json (100%) rename simulations/{vip-666 => vip-616}/arbitrumone.ts (100%) rename simulations/{vip-666 => vip-616}/basemainnet.ts (100%) rename simulations/{vip-666 => vip-616}/ethereum.ts (100%) rename simulations/{vip-666 => vip-616}/sepolia.ts (100%) rename simulations/{vip-666 => vip-616}/shared.ts (100%) rename simulations/{vip-667 => vip-617}/arbitrumone.ts (100%) rename simulations/{vip-667 => vip-617}/basemainnet.ts (100%) rename simulations/{vip-667 => vip-617}/ethereum.ts (100%) rename simulations/{vip-667 => vip-617}/shared.ts (100%) rename vips/{vip-666 => vip-616}/addresses/arbitrumone.ts (100%) rename vips/{vip-666 => vip-616}/addresses/basemainnet.ts (100%) rename vips/{vip-666 => vip-616}/addresses/ethereum.ts (100%) rename vips/{vip-666 => vip-616}/addresses/sepolia.ts (100%) rename vips/{vip-666 => vip-616}/bscmainnet.ts (100%) rename vips/{vip-666 => vip-616}/bsctestnet.ts (100%) rename vips/{vip-667 => vip-617}/bscmainnet.ts (100%) diff --git a/simulations/vip-666/abi/AccessControlManager.json b/simulations/vip-616/abi/AccessControlManager.json similarity index 100% rename from simulations/vip-666/abi/AccessControlManager.json rename to simulations/vip-616/abi/AccessControlManager.json diff --git a/simulations/vip-666/abi/AerodromeSlipstreamOracle.json b/simulations/vip-616/abi/AerodromeSlipstreamOracle.json similarity index 100% rename from simulations/vip-666/abi/AerodromeSlipstreamOracle.json rename to simulations/vip-616/abi/AerodromeSlipstreamOracle.json diff --git a/simulations/vip-666/abi/CurveOracle.json b/simulations/vip-616/abi/CurveOracle.json similarity index 100% rename from simulations/vip-666/abi/CurveOracle.json rename to simulations/vip-616/abi/CurveOracle.json diff --git a/simulations/vip-666/abi/DeviationSentinel.json b/simulations/vip-616/abi/DeviationSentinel.json similarity index 100% rename from simulations/vip-666/abi/DeviationSentinel.json rename to simulations/vip-616/abi/DeviationSentinel.json diff --git a/simulations/vip-666/abi/EBrake.json b/simulations/vip-616/abi/EBrake.json similarity index 100% rename from simulations/vip-666/abi/EBrake.json rename to simulations/vip-616/abi/EBrake.json diff --git a/simulations/vip-666/abi/ILComptroller.json b/simulations/vip-616/abi/ILComptroller.json similarity index 100% rename from simulations/vip-666/abi/ILComptroller.json rename to simulations/vip-616/abi/ILComptroller.json diff --git a/simulations/vip-666/abi/ResilientOracle.json b/simulations/vip-616/abi/ResilientOracle.json similarity index 100% rename from simulations/vip-666/abi/ResilientOracle.json rename to simulations/vip-616/abi/ResilientOracle.json diff --git a/simulations/vip-666/abi/SentinelOracle.json b/simulations/vip-616/abi/SentinelOracle.json similarity index 100% rename from simulations/vip-666/abi/SentinelOracle.json rename to simulations/vip-616/abi/SentinelOracle.json diff --git a/simulations/vip-666/abi/UniswapOracle.json b/simulations/vip-616/abi/UniswapOracle.json similarity index 100% rename from simulations/vip-666/abi/UniswapOracle.json rename to simulations/vip-616/abi/UniswapOracle.json diff --git a/simulations/vip-666/abi/VToken.json b/simulations/vip-616/abi/VToken.json similarity index 100% rename from simulations/vip-666/abi/VToken.json rename to simulations/vip-616/abi/VToken.json diff --git a/simulations/vip-666/arbitrumone.ts b/simulations/vip-616/arbitrumone.ts similarity index 100% rename from simulations/vip-666/arbitrumone.ts rename to simulations/vip-616/arbitrumone.ts diff --git a/simulations/vip-666/basemainnet.ts b/simulations/vip-616/basemainnet.ts similarity index 100% rename from simulations/vip-666/basemainnet.ts rename to simulations/vip-616/basemainnet.ts diff --git a/simulations/vip-666/ethereum.ts b/simulations/vip-616/ethereum.ts similarity index 100% rename from simulations/vip-666/ethereum.ts rename to simulations/vip-616/ethereum.ts diff --git a/simulations/vip-666/sepolia.ts b/simulations/vip-616/sepolia.ts similarity index 100% rename from simulations/vip-666/sepolia.ts rename to simulations/vip-616/sepolia.ts diff --git a/simulations/vip-666/shared.ts b/simulations/vip-616/shared.ts similarity index 100% rename from simulations/vip-666/shared.ts rename to simulations/vip-616/shared.ts diff --git a/simulations/vip-667/arbitrumone.ts b/simulations/vip-617/arbitrumone.ts similarity index 100% rename from simulations/vip-667/arbitrumone.ts rename to simulations/vip-617/arbitrumone.ts diff --git a/simulations/vip-667/basemainnet.ts b/simulations/vip-617/basemainnet.ts similarity index 100% rename from simulations/vip-667/basemainnet.ts rename to simulations/vip-617/basemainnet.ts diff --git a/simulations/vip-667/ethereum.ts b/simulations/vip-617/ethereum.ts similarity index 100% rename from simulations/vip-667/ethereum.ts rename to simulations/vip-617/ethereum.ts diff --git a/simulations/vip-667/shared.ts b/simulations/vip-617/shared.ts similarity index 100% rename from simulations/vip-667/shared.ts rename to simulations/vip-617/shared.ts diff --git a/vips/vip-666/addresses/arbitrumone.ts b/vips/vip-616/addresses/arbitrumone.ts similarity index 100% rename from vips/vip-666/addresses/arbitrumone.ts rename to vips/vip-616/addresses/arbitrumone.ts diff --git a/vips/vip-666/addresses/basemainnet.ts b/vips/vip-616/addresses/basemainnet.ts similarity index 100% rename from vips/vip-666/addresses/basemainnet.ts rename to vips/vip-616/addresses/basemainnet.ts diff --git a/vips/vip-666/addresses/ethereum.ts b/vips/vip-616/addresses/ethereum.ts similarity index 100% rename from vips/vip-666/addresses/ethereum.ts rename to vips/vip-616/addresses/ethereum.ts diff --git a/vips/vip-666/addresses/sepolia.ts b/vips/vip-616/addresses/sepolia.ts similarity index 100% rename from vips/vip-666/addresses/sepolia.ts rename to vips/vip-616/addresses/sepolia.ts diff --git a/vips/vip-666/bscmainnet.ts b/vips/vip-616/bscmainnet.ts similarity index 100% rename from vips/vip-666/bscmainnet.ts rename to vips/vip-616/bscmainnet.ts diff --git a/vips/vip-666/bsctestnet.ts b/vips/vip-616/bsctestnet.ts similarity index 100% rename from vips/vip-666/bsctestnet.ts rename to vips/vip-616/bsctestnet.ts diff --git a/vips/vip-667/bscmainnet.ts b/vips/vip-617/bscmainnet.ts similarity index 100% rename from vips/vip-667/bscmainnet.ts rename to vips/vip-617/bscmainnet.ts From a7aacfdadc4c9534c815efb8b3118bbf1e3e9d1d Mon Sep 17 00:00:00 2001 From: Fred Date: Sun, 3 May 2026 09:17:17 +0000 Subject: [PATCH 12/15] chore: update VIP titles, descriptions, and cross-references for renumber --- simulations/vip-616/arbitrumone.ts | 6 ++-- simulations/vip-616/basemainnet.ts | 6 ++-- simulations/vip-616/ethereum.ts | 6 ++-- simulations/vip-616/sepolia.ts | 6 ++-- simulations/vip-616/shared.ts | 26 ++++++++--------- simulations/vip-617/arbitrumone.ts | 6 ++-- simulations/vip-617/basemainnet.ts | 6 ++-- simulations/vip-617/ethereum.ts | 6 ++-- simulations/vip-617/shared.ts | 44 ++++++++++++++-------------- vips/vip-616/bscmainnet.ts | 46 +++++++++++------------------- vips/vip-617/bscmainnet.ts | 41 ++++++++++++++------------ 11 files changed, 94 insertions(+), 105 deletions(-) diff --git a/simulations/vip-616/arbitrumone.ts b/simulations/vip-616/arbitrumone.ts index e0c637ba0..6ca0b25c3 100644 --- a/simulations/vip-616/arbitrumone.ts +++ b/simulations/vip-616/arbitrumone.ts @@ -1,10 +1,10 @@ import { forking } from "src/vip-framework"; -import { ARBITRUMONE_CONFIG } from "../../vips/vip-666/addresses/arbitrumone"; -import { runVip666Suite } from "./shared"; +import { ARBITRUMONE_CONFIG } from "../../vips/vip-616/addresses/arbitrumone"; +import { runVip616Suite } from "./shared"; const FORK_BLOCK = 457580585; forking(FORK_BLOCK, async () => { - await runVip666Suite(ARBITRUMONE_CONFIG); + await runVip616Suite(ARBITRUMONE_CONFIG); }); diff --git a/simulations/vip-616/basemainnet.ts b/simulations/vip-616/basemainnet.ts index 51bf24bcf..eb8434ac8 100644 --- a/simulations/vip-616/basemainnet.ts +++ b/simulations/vip-616/basemainnet.ts @@ -1,10 +1,10 @@ import { forking } from "src/vip-framework"; -import { BASEMAINNET_CONFIG } from "../../vips/vip-666/addresses/basemainnet"; -import { runVip666Suite } from "./shared"; +import { BASEMAINNET_CONFIG } from "../../vips/vip-616/addresses/basemainnet"; +import { runVip616Suite } from "./shared"; const FORK_BLOCK = 45338981; forking(FORK_BLOCK, async () => { - await runVip666Suite(BASEMAINNET_CONFIG); + await runVip616Suite(BASEMAINNET_CONFIG); }); diff --git a/simulations/vip-616/ethereum.ts b/simulations/vip-616/ethereum.ts index 5b7950655..b526ebb28 100644 --- a/simulations/vip-616/ethereum.ts +++ b/simulations/vip-616/ethereum.ts @@ -1,10 +1,10 @@ import { forking } from "src/vip-framework"; -import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; -import { runVip666Suite } from "./shared"; +import { ETHEREUM_CONFIG } from "../../vips/vip-616/addresses/ethereum"; +import { runVip616Suite } from "./shared"; const FORK_BLOCK = 24992853; forking(FORK_BLOCK, async () => { - await runVip666Suite(ETHEREUM_CONFIG); + await runVip616Suite(ETHEREUM_CONFIG); }); diff --git a/simulations/vip-616/sepolia.ts b/simulations/vip-616/sepolia.ts index 957aee47a..e58f3bebb 100644 --- a/simulations/vip-616/sepolia.ts +++ b/simulations/vip-616/sepolia.ts @@ -6,17 +6,17 @@ import { ZERO_ADDRESS } from "src/networkAddresses"; import { expectEvents, initMainnetUser } from "src/utils"; import { forking, testForkedNetworkVipCommands } from "src/vip-framework"; -import { SEPOLIA_CONFIG, SEPOLIA_GUARDIAN_OWNER } from "../../vips/vip-666/addresses/sepolia"; +import { SEPOLIA_CONFIG, SEPOLIA_GUARDIAN_OWNER } from "../../vips/vip-616/addresses/sepolia"; import { DIAMOND_ONLY_EBRAKE_PERMS, EBRAKE_COMPTROLLER_PERMS_IL, SENTINEL_EBRAKE_PERMS, -} from "../../vips/vip-666/bscmainnet"; +} from "../../vips/vip-616/bscmainnet"; import vip666Sepolia, { DEPLOYER_COMPTROLLER_PERMS, DEPLOYER_EBRAKE_PERMS, DEPLOYER_SENTINEL_ORACLE_PERMS, -} from "../../vips/vip-666/bsctestnet"; +} from "../../vips/vip-616/bsctestnet"; import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; import DEVIATION_SENTINEL_ABI from "./abi/DeviationSentinel.json"; import EBRAKE_ABI from "./abi/EBrake.json"; diff --git a/simulations/vip-616/shared.ts b/simulations/vip-616/shared.ts index d076f8ac6..88b22c66d 100644 --- a/simulations/vip-616/shared.ts +++ b/simulations/vip-616/shared.ts @@ -6,7 +6,7 @@ import { ZERO_ADDRESS } from "src/networkAddresses"; import { expectEvents, initMainnetUser } from "src/utils"; import { testForkedNetworkVipCommands } from "src/vip-framework"; -import vip666, { +import vip616, { AERODROME_ORACLE_ADMIN_PERMS, CURVE_ORACLE_ADMIN_PERMS, ChainConfig, @@ -19,7 +19,7 @@ import vip666, { SENTINEL_ORACLE_ADMIN_PERMS, UNISWAP_ORACLE_ADMIN_PERMS, governanceAccounts, -} from "../../vips/vip-666/bscmainnet"; +} from "../../vips/vip-616/bscmainnet"; import ACCESS_CONTROL_MANAGER_ABI from "./abi/AccessControlManager.json"; import AERODROME_ORACLE_ABI from "./abi/AerodromeSlipstreamOracle.json"; import CURVE_ORACLE_ABI from "./abi/CurveOracle.json"; @@ -28,7 +28,7 @@ import EBRAKE_ABI from "./abi/EBrake.json"; import SENTINEL_ORACLE_ABI from "./abi/SentinelOracle.json"; import UNISWAP_ORACLE_ABI from "./abi/UniswapOracle.json"; -// RoleGranted events emitted by VIP-666 (Sub-A). Per-chain count is variable because +// RoleGranted events emitted by VIP-616 (Sub-A). Per-chain count is variable because // CurveOracle (Ethereum) and AerodromeSlipstreamOracle (Base) each add 4 admin grants. // base = admin grants (12+8+4) + ebrake→comptroller (4) + reset (12) + sentinel→ebrake (3) + multisig (8) = 51 // +4 if cfg.curveOracle is set, +4 if cfg.aerodromeOracle is set @@ -56,10 +56,10 @@ const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { return missing; }; -export const runVip666Suite = async (cfg: ChainConfig) => { +export const runVip616Suite = async (cfg: ChainConfig) => { const missing = collectMissingPlaceholders(cfg); if (missing.length > 0) { - describe.skip(`VIP-666 [${cfg.name}] — placeholder addresses missing: ${missing.join(", ")}`, () => { + describe.skip(`VIP-616 [${cfg.name}] — placeholder addresses missing: ${missing.join(", ")}`, () => { it(`Fill ${missing.join(", ")} in addresses/${cfg.name .toLowerCase() .replace(/\s/g, "")}.ts to run this suite`, () => { @@ -111,7 +111,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { } }); - describe(`VIP-666 [${cfg.name}] — Pre-VIP behaviour`, () => { + describe(`VIP-616 [${cfg.name}] — Pre-VIP behaviour`, () => { it("DeviationSentinel pendingOwner is Normal Timelock", async () => { expect(await deviationSentinel.pendingOwner()).to.equal(cfg.normalTimelock); }); @@ -233,7 +233,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { }); }); - testForkedNetworkVipCommands(`VIP-666 [${cfg.name}] Bootstrap & Permissions`, await vip666(), { + testForkedNetworkVipCommands(`VIP-616 [${cfg.name}] Bootstrap & Permissions`, await vip616(), { callbackAfterExecution: async txResponse => { // 4 + (1 each for CurveOracle / AerodromeSlipstreamOracle) acceptOwnership() calls await expectEvents( @@ -244,13 +244,13 @@ export const runVip666Suite = async (cfg: ChainConfig) => { ); // 5 trusted keepers whitelisted per chain await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TrustedKeeperUpdated"], [5]); - // RoleGranted events (Sub-A only — governance EBrake actions deferred to VIP-667). + // RoleGranted events (Sub-A only — governance EBrake actions deferred to VIP-617). // Variable per chain depending on which optional DEX oracles are present. await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [expectedPermsGranted(cfg)]); }, }); - describe(`VIP-666 [${cfg.name}] — Post-VIP behaviour`, () => { + describe(`VIP-616 [${cfg.name}] — Post-VIP behaviour`, () => { it("All bootstrap contracts have Normal Timelock as owner", async () => { expect(await deviationSentinel.owner()).to.equal(cfg.normalTimelock); expect(await sentinelOracle.owner()).to.equal(cfg.normalTimelock); @@ -363,13 +363,13 @@ export const runVip666Suite = async (cfg: ChainConfig) => { }); // Sub-A intentionally does NOT grant governance EBrake action perms or wire markets - // — those land in VIP-667. Assert the deferred state explicitly so a regression is loud. - it("Guardian + Timelocks still have no EBrake-specific action permissions (deferred to VIP-667)", async () => { + // — those land in VIP-617. Assert the deferred state explicitly so a regression is loud. + it("Guardian + Timelocks still have no EBrake-specific action permissions (deferred to VIP-617)", async () => { for (const account of govAccounts) { for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal( false, - `unexpected ${sig} for ${account} — should be granted by VIP-667`, + `unexpected ${sig} for ${account} — should be granted by VIP-617`, ); } } @@ -377,7 +377,7 @@ export const runVip666Suite = async (cfg: ChainConfig) => { for (const market of cfg.monitoredMarkets) { if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; - it(`${market.symbol} is still not wired (deferred to VIP-667)`, async () => { + it(`${market.symbol} is still not wired (deferred to VIP-617)`, async () => { // Each market's wiring lives on its routed DEX oracle; assert its slot is empty. const oracleType = market.oracleType ?? "uniswap"; if (oracleType === "curve") { diff --git a/simulations/vip-617/arbitrumone.ts b/simulations/vip-617/arbitrumone.ts index 24b2859a7..21b259372 100644 --- a/simulations/vip-617/arbitrumone.ts +++ b/simulations/vip-617/arbitrumone.ts @@ -1,10 +1,10 @@ import { forking } from "src/vip-framework"; -import { ARBITRUMONE_CONFIG } from "../../vips/vip-666/addresses/arbitrumone"; -import { runVip667Suite } from "./shared"; +import { ARBITRUMONE_CONFIG } from "../../vips/vip-616/addresses/arbitrumone"; +import { runVip617Suite } from "./shared"; const FORK_BLOCK = 457434103; forking(FORK_BLOCK, async () => { - await runVip667Suite(ARBITRUMONE_CONFIG); + await runVip617Suite(ARBITRUMONE_CONFIG); }); diff --git a/simulations/vip-617/basemainnet.ts b/simulations/vip-617/basemainnet.ts index 68df03a5e..147120495 100644 --- a/simulations/vip-617/basemainnet.ts +++ b/simulations/vip-617/basemainnet.ts @@ -1,11 +1,11 @@ import { forking } from "src/vip-framework"; -import { BASEMAINNET_CONFIG } from "../../vips/vip-666/addresses/basemainnet"; -import { runVip667Suite } from "./shared"; +import { BASEMAINNET_CONFIG } from "../../vips/vip-616/addresses/basemainnet"; +import { runVip617Suite } from "./shared"; // Must be ≥ 45337626 — AerodromeSlipstreamOracle proxy deployment block. const FORK_BLOCK = 45338981; forking(FORK_BLOCK, async () => { - await runVip667Suite(BASEMAINNET_CONFIG); + await runVip617Suite(BASEMAINNET_CONFIG); }); diff --git a/simulations/vip-617/ethereum.ts b/simulations/vip-617/ethereum.ts index 5e0a82c39..252f7e033 100644 --- a/simulations/vip-617/ethereum.ts +++ b/simulations/vip-617/ethereum.ts @@ -1,11 +1,11 @@ import { forking } from "src/vip-framework"; -import { ETHEREUM_CONFIG } from "../../vips/vip-666/addresses/ethereum"; -import { runVip667Suite } from "./shared"; +import { ETHEREUM_CONFIG } from "../../vips/vip-616/addresses/ethereum"; +import { runVip617Suite } from "./shared"; // Must be ≥ 24985630 — CurveOracle proxy deployment block. const FORK_BLOCK = 24992834; forking(FORK_BLOCK, async () => { - await runVip667Suite(ETHEREUM_CONFIG); + await runVip617Suite(ETHEREUM_CONFIG); }); diff --git a/simulations/vip-617/shared.ts b/simulations/vip-617/shared.ts index 5eb9a456d..cfede0dfd 100644 --- a/simulations/vip-617/shared.ts +++ b/simulations/vip-617/shared.ts @@ -5,24 +5,24 @@ import { ZERO_ADDRESS } from "src/networkAddresses"; import { expectEvents, getForkedNetworkAddress, setMaxStalePeriodInChainlinkOracle } from "src/utils"; import { testForkedNetworkVipCommands } from "src/vip-framework"; -import vip666, { +import vip616, { ChainConfig, GOVERNANCE_EBRAKE_PERMS_IL, MonitoredMarket, governanceAccounts, -} from "../../vips/vip-666/bscmainnet"; -import vip667 from "../../vips/vip-667/bscmainnet"; -import ACCESS_CONTROL_MANAGER_ABI from "../vip-666/abi/AccessControlManager.json"; -import AERODROME_ORACLE_ABI from "../vip-666/abi/AerodromeSlipstreamOracle.json"; -import CURVE_ORACLE_ABI from "../vip-666/abi/CurveOracle.json"; -import DEVIATION_SENTINEL_ABI from "../vip-666/abi/DeviationSentinel.json"; -import IL_COMPTROLLER_ABI from "../vip-666/abi/ILComptroller.json"; -import RESILIENT_ORACLE_ABI from "../vip-666/abi/ResilientOracle.json"; -import SENTINEL_ORACLE_ABI from "../vip-666/abi/SentinelOracle.json"; -import UNISWAP_ORACLE_ABI from "../vip-666/abi/UniswapOracle.json"; -import VTOKEN_ABI from "../vip-666/abi/VToken.json"; +} from "../../vips/vip-616/bscmainnet"; +import vip617 from "../../vips/vip-617/bscmainnet"; +import ACCESS_CONTROL_MANAGER_ABI from "../vip-616/abi/AccessControlManager.json"; +import AERODROME_ORACLE_ABI from "../vip-616/abi/AerodromeSlipstreamOracle.json"; +import CURVE_ORACLE_ABI from "../vip-616/abi/CurveOracle.json"; +import DEVIATION_SENTINEL_ABI from "../vip-616/abi/DeviationSentinel.json"; +import IL_COMPTROLLER_ABI from "../vip-616/abi/ILComptroller.json"; +import RESILIENT_ORACLE_ABI from "../vip-616/abi/ResilientOracle.json"; +import SENTINEL_ORACLE_ABI from "../vip-616/abi/SentinelOracle.json"; +import UNISWAP_ORACLE_ABI from "../vip-616/abi/UniswapOracle.json"; +import VTOKEN_ABI from "../vip-616/abi/VToken.json"; -// RoleGranted events emitted by VIP-667 (Sub-B) per chain: +// RoleGranted events emitted by VIP-617 (Sub-B) per chain: // 8 (governance ebrake action) × 4 = 32 (token wiring uses direct setter calls) const PERMS_GRANTED_PER_CHAIN = 32; @@ -108,11 +108,11 @@ const buildVTokenIndex = async (comptrollerAddress: string): Promise { +export const runVip617Suite = async (cfg: ChainConfig) => { const missing = collectMissingPlaceholders(cfg); if (missing.length > 0) { - describe.skip(`VIP-667 [${cfg.name}] — placeholder addresses missing: ${missing.join(", ")}`, () => { - it(`Fill ${missing.join(", ")} in vips/vip-666/addresses/${cfg.name + describe.skip(`VIP-617 [${cfg.name}] — placeholder addresses missing: ${missing.join(", ")}`, () => { + it(`Fill ${missing.join(", ")} in vips/vip-616/addresses/${cfg.name .toLowerCase() .replace(/\s/g, "")}.ts to run this suite`, () => { // intentionally empty — skip stub @@ -146,7 +146,7 @@ export const runVip667Suite = async (cfg: ChainConfig) => { vTokenByUnderlying = await buildVTokenIndex(cfg.comptroller); }); - describe(`VIP-667 [${cfg.name}] — Monitored markets config validity`, () => { + describe(`VIP-617 [${cfg.name}] — Monitored markets config validity`, () => { // A zero-address token or pool, or an out-of-range deviation, would be silently // skipped or accepted-as-malformed by the wiring loop. Fail loud at simulation time. it("Every monitored market has non-zero token, non-zero pool, and 0 < deviation ≤ 100", () => { @@ -171,11 +171,11 @@ export const runVip667Suite = async (cfg: ChainConfig) => { }); }); - // VIP-667 depends on VIP-666 having been executed — apply it first within the + // VIP-617 depends on VIP-616 having been executed — apply it first within the // same fork so post-A state is the pre-VIP state for B. - testForkedNetworkVipCommands(`VIP-666 [${cfg.name}] (prerequisite for VIP-667)`, await vip666()); + testForkedNetworkVipCommands(`VIP-616 [${cfg.name}] (prerequisite for VIP-617)`, await vip616()); - describe(`VIP-667 [${cfg.name}] — Pre-VIP behaviour (post-VIP-666 state)`, () => { + describe(`VIP-617 [${cfg.name}] — Pre-VIP behaviour (post-VIP-616 state)`, () => { it("Guardian + Timelocks have no EBrake-specific action permissions yet", async () => { for (const account of govAccounts) { for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { @@ -208,7 +208,7 @@ export const runVip667Suite = async (cfg: ChainConfig) => { } }); - testForkedNetworkVipCommands(`VIP-667 [${cfg.name}] Governance Actions & Market Wiring`, await vip667(), { + testForkedNetworkVipCommands(`VIP-617 [${cfg.name}] Governance Actions & Market Wiring`, await vip617(), { callbackAfterExecution: async txResponse => { // 32 RoleGranted events per chain (governance EBrake action perms) await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [PERMS_GRANTED_PER_CHAIN]); @@ -233,7 +233,7 @@ export const runVip667Suite = async (cfg: ChainConfig) => { }, }); - describe(`VIP-667 [${cfg.name}] — Post-VIP behaviour`, () => { + describe(`VIP-617 [${cfg.name}] — Post-VIP behaviour`, () => { it("Guardian + Timelocks have all 8 IL-supported EBrake action permissions", async () => { for (const account of govAccounts) { for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { diff --git a/vips/vip-616/bscmainnet.ts b/vips/vip-616/bscmainnet.ts index b7e8296eb..6cfacda75 100644 --- a/vips/vip-616/bscmainnet.ts +++ b/vips/vip-616/bscmainnet.ts @@ -127,8 +127,8 @@ export const grant = (acm: string, contract: string, sig: string, account: strin dstChainId, }); -// VIP-666 (Sub-A): bootstrap + permissions. Kept under each chain's block gas -// limit by deferring governance EBrake action grants and market wiring to VIP-667. +// VIP-616 (Sub-A): bootstrap + permissions. Kept under each chain's block gas +// limit by deferring governance EBrake action grants and market wiring to VIP-617. // Per-chain command count varies with the optional CurveOracle (Ethereum) and // AerodromeSlipstreamOracle (Base): each adds 1 acceptOwnership + 4 admin grants. // - Ethereum: 60 + 5 (CurveOracle) = 65 @@ -208,40 +208,26 @@ const buildChainCommandsA = (cfg: ChainConfig): Command[] => { ]; }; -export const vip666 = () => { +export const vip616 = () => { const meta = { version: "v2", title: - "VIP-666 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2 — Bootstrap & Permissions (1/2)", - description: `#### Description + "VIP-616 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2 — Bootstrap & Permissions (1/2)", + description: `#### Context -This is the first of two VIPs that configure the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, and **Base**, mirroring the BSC setup from VIP-590 + VIP-610. Each chain's DeviationSentinel routes automated oracle-deviation enforcement through a local EBrakeV2, which applies per-action, per-market restrictions (pause borrow/supply, zero collateral factor) without manual intervention. +Deploys the DeviationSentinel + EBrakeV2 stack on the three non-BSC chains — the same oracle-manipulation protection layer that's been running on BSC since VIP-590 / VIP-610. This VIP accepts ownership of the newly deployed contracts and wires up the permissions between Sentinel, EBrake, the IL Comptroller, governance, and the Multisig Pauser. The actual market monitoring is turned on in VIP-617. -The configuration is split across two VIPs so each per-chain payload fits under the destination chain's block gas limit: +#### Per-chain Actions (×3 chains) -- **VIP-666 (this VIP)** — accept ownership, grant admin/reset/sentinel→ebrake/ebrake→comptroller/multisig permissions, whitelist trusted keepers -- **VIP-667 (follow-up)** — grant Guardian + Timelocks the IL-supported EBrake action permissions, then wire each monitored market on the appropriate DEX oracle (UniswapOracle / CurveOracle / AerodromeSlipstreamOracle), SentinelOracle, and DeviationSentinel +For each chain, the VIP performs the following 7 steps: -In addition to UniswapOracle, two extra DEX oracles are bootstrapped on the chains that need them: - -- **CurveOracle (Ethereum only)** — prices eBTC against WBTC via the eBTC/WBTC Curve StableSwap-NG pool's EMA \`price_oracle\`. Required because Curve StableSwap-NG pools are not Uniswap V3 ABI-compatible. -- **AerodromeSlipstreamOracle (Base only)** — prices cbBTC and wstETH on the most liquid Aerodrome Slipstream pools (cbBTC/USDC and wstETH/WETH). Aerodrome Slipstream's \`slot0()\` returns a 6-tuple (no \`feeProtocol\`) so the Solidity decoder reverts when read against the Uniswap V3 7-tuple ABI used by UniswapOracle. - -Because EBrake on these chains uses \`isIsolatedPool=true\` (single-pool IL Comptroller, not the BSC Diamond), only the IL-supported subset of EBrake action functions is granted. Diamond-only functions (\`pauseFlashLoan\`, \`disablePoolBorrow\`, \`revokeFlashLoanAccess\`, \`decreaseCF(address,uint96,uint256)\`) are omitted as they revert on IL comptrollers. - -#### Summary - -If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: - -- Accept governance ownership of the **DeviationSentinel**, **SentinelOracle**, **UniswapOracle**, and **EBrakeV2** contracts (plus **CurveOracle** on Ethereum and **AerodromeSlipstreamOracle** on Base) -- Grant admin permissions on DeviationSentinel, SentinelOracle, UniswapOracle, and (where present) CurveOracle / AerodromeSlipstreamOracle to Guardian + 3 Timelocks -- Grant **EBrakeV2** the 4 IL-supported Comptroller permissions it needs to execute emergency actions -- Authorize **DeviationSentinel** to call \`pauseBorrow\`, \`pauseSupply\`, and \`decreaseCF\` on EBrake -- Grant **Guardian** and governance **Timelocks** the granular snapshot-reset permissions on EBrake -- Grant the **per-chain 1-of-1 Multisig Pauser** the 8 IL-supported EBrake action functions for manual emergency pausing (Phase 0) -- Whitelist Keeper + Guardian + 3 Timelocks as trusted keepers on DeviationSentinel - -**Permission event summary**: 161 PermissionGranted (Ethereum 55 + Arbitrum One 51 + Base 55), 0 PermissionRevoked +1. **Accept ownership** of the newly deployed contracts: DeviationSentinel, SentinelOracle, UniswapOracle, EBrake (+ CurveOracle on Ethereum, + AerodromeSlipstreamOracle on Base). +2. **Grant admin perms** to Guardian + 3 Timelocks (Normal / Fast-track / Critical) on each oracle and the DeviationSentinel — so governance can update pool configs and monitoring settings. +3. **Authorize EBrake → IL Comptroller** for the 4 emergency action types (pause, collateral factor, borrow cap, supply cap). +4. **Grant snapshot-reset perms on EBrake** to Guardian + 3 Timelocks — so governance can restore market config after a brake event fires. +5. **Authorize DeviationSentinel → EBrake** for the 3 actions Sentinel auto-invokes when a deviation triggers (pause borrow, pause supply, decrease CF). +6. **Grant the Multisig Pauser the 8 IL-supported EBrake actions** — manual emergency control during the early operational phase. +7. **Whitelist trusted keepers** on DeviationSentinel — the off-chain keeper + Guardian + 3 Timelocks. #### References @@ -257,4 +243,4 @@ If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: return makeProposal(NETWORKS.flatMap(buildChainCommandsA), meta, ProposalType.REGULAR); }; -export default vip666; +export default vip616; diff --git a/vips/vip-617/bscmainnet.ts b/vips/vip-617/bscmainnet.ts index aea1078bc..f914861b1 100644 --- a/vips/vip-617/bscmainnet.ts +++ b/vips/vip-617/bscmainnet.ts @@ -2,16 +2,16 @@ import { ZERO_ADDRESS } from "src/networkAddresses"; import { Command, ProposalType } from "src/types"; import { makeProposal } from "src/utils"; -import { ARBITRUMONE_CONFIG } from "../vip-666/addresses/arbitrumone"; -import { BASEMAINNET_CONFIG } from "../vip-666/addresses/basemainnet"; -import { ETHEREUM_CONFIG } from "../vip-666/addresses/ethereum"; +import { ARBITRUMONE_CONFIG } from "../vip-616/addresses/arbitrumone"; +import { BASEMAINNET_CONFIG } from "../vip-616/addresses/basemainnet"; +import { ETHEREUM_CONFIG } from "../vip-616/addresses/ethereum"; import { ChainConfig, GOVERNANCE_EBRAKE_PERMS_IL, MonitoredMarket, governanceAccounts, grant, -} from "../vip-666/bscmainnet"; +} from "../vip-616/bscmainnet"; const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, BASEMAINNET_CONFIG]; @@ -70,7 +70,7 @@ const buildSetPoolCommand = (cfg: ChainConfig, market: MonitoredMarket, dstChain }; }; -// VIP-667 (Sub-B): governance EBrake action grants + per-market wiring. +// VIP-617 (Sub-B): governance EBrake action grants + per-market wiring. // Per-chain command count: gov ebrake action 32 + 3 × eligible markets. // - Ethereum: 32 + 30 (10 mkts: 9 Uniswap + 1 Curve) = 62 // - Arbitrum: 32 + 15 ( 5 mkts: 5 Uniswap) = 47 @@ -114,31 +114,34 @@ const buildChainCommandsB = (cfg: ChainConfig): Command[] => { return commands; }; -export const vip667 = () => { +export const vip617 = () => { const meta = { version: "v2", title: - "VIP-667 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2 — Governance Actions & Market Wiring (2/2)", - description: `#### Description + "VIP-617 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2 — Governance Actions & Market Wiring (2/2)", + description: `#### Context -This is the second of two VIPs configuring the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, and **Base**. It depends on VIP-666 (Bootstrap & Permissions) being executed first. +With ownership and permissions in place from VIP-616, this VIP grants governance the ability to manually invoke EBrake actions and turns on deviation monitoring for the 19 eligible markets at a unified 10% threshold. -Splitting the configuration across two VIPs keeps each per-chain payload under the destination chain's block gas limit. VIP-666 covers ownership, admin permissions, reset / sentinel → ebrake / ebrake → comptroller / multisig grants, and trusted-keeper whitelisting. This VIP covers governance EBrake action permissions and per-market deviation wiring. +#### Per-chain Actions -Because EBrake on these chains uses \`isIsolatedPool=true\`, only the IL-supported subset of action functions is granted (Diamond-only functions revert on IL comptrollers). +For each chain, the VIP performs the following 2 blocks of work: -#### Summary +1. **Grant the 8 IL-supported EBrake action perms** to Guardian + 3 Timelocks (same set Multisig Pauser receives in VIP-616 step 6). +2. **For each monitored market** — 3 calls per market: + - Bind the underlying token to its DEX pool on the appropriate DEX oracle + - Route SentinelOracle to that DEX oracle for the token + - Enable DeviationSentinel monitoring at the 10% threshold -If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: +#### DEX oracles routed -- Grant **Guardian** and governance **Timelocks** the 8 IL-supported EBrake action functions -- Configure deviation monitoring (10% threshold) for the eligible Core Pool markets on each chain — 10 on Ethereum (9 Uniswap V3 + 1 Curve / eBTC), 5 on Arbitrum One (Uniswap V3), 4 on Base (2 Uniswap V3 + 2 Aerodrome Slipstream / cbBTC + wstETH) - -**Permission event summary**: 96 PermissionGranted (32 per chain × 3 chains), 0 PermissionRevoked +- **Uniswap V3** (UniswapOracle) — default, covers most markets on all 3 chains +- **Curve** (CurveOracle) — Ethereum only, for the eBTC/WBTC StableSwap-NG pool +- **Aerodrome Slipstream** (AerodromeSlipstreamOracle) — Base only, for cbBTC + wstETH #### References -- [VIP-666 (Bootstrap & Permissions)](https://app.venus.io/governance/proposal/666) +- [VIP-616 (Bootstrap & Permissions)](https://app.venus.io/governance/proposal/616) - [VIP-590 (BSC)](https://app.venus.io/governance/proposal/590) - [VIP-610 (BSC)](https://app.venus.io/governance/proposal/610) - [Original Proposal: Emergency Brake — Price Deviation Safeguard Mechanism](https://community.venus.io/t/proposal-emergency-brake-price-deviation-safeguard-mechanism/5668) @@ -151,4 +154,4 @@ If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: return makeProposal(NETWORKS.flatMap(buildChainCommandsB), meta, ProposalType.REGULAR); }; -export default vip667; +export default vip617; From 12f353c5032f0a5080c65550ea3e42daf20fd49f Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 4 May 2026 12:14:53 +0530 Subject: [PATCH 13/15] feat(vip-framework): assert per-chain LZ payload size in makeProposal Cross-chain commands fail at on-chain estimateFees with 'Relayer: _payloadSize tooooo big' when the encoded payload exceeds 10,000 bytes, but local simulations bypass the Relayer and pass silently. Compute the payloadWithId envelope size during proposal build and throw with the offending dstChainId + command count so oversized VIPs fail in CI rather than at proposeOnTestnet/propose time. --- src/utils.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/utils.ts b/src/utils.ts index eee4e3da3..090b3b72c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -30,6 +30,15 @@ import COMPTROLLER_ABI from "./vip-framework/abi/comptroller.json"; const BSCTESTNET_OMNICHAIN_SENDER = "0xCfD34AEB46b1CB4779c945854d405E91D27A1899"; const BSCMAINNET_OMNICHAIN_SENDER = "0x36a69dE601381be7b0DcAc5D5dD058825505F8f6"; +// LayerZero payload size cap. Verified on-chain across every destination +// OmnichainGovernanceExecutor Venus supports (ethereum, arbitrumone, opmainnet, basemainnet, +// zksyncmainnet, opbnbmainnet, unichainmainnet, plus the matching testnets): +// DEFAULT_PAYLOAD_SIZE_LIMIT = 10000 and payloadSizeLimitLookup(bsc src) = 0 (no override) +// on every executor. The LZ Relayer enforces the same 10000-byte cap on the sender side, +// so this single constant is the binding limit for every cross-chain VIP. If any executor +// later sets a per-source override, switch to a runtime lookup. +const LZ_PAYLOAD_SIZE_LIMIT = 10_000; + export const getOmnichainProposalSenderAddress = () => { if (FORKED_NETWORK === "bscmainnet" || REMOTE_MAINNET_NETWORKS.includes(FORKED_NETWORK as REMOTE_NETWORKS)) { return BSCMAINNET_OMNICHAIN_SENDER; @@ -240,6 +249,19 @@ export const makeProposal = async ( ); const remoteAdapterParam = getAdapterParam(chainCommands.map(cmd => cmd.target).length); + // LZ Relayer sizes the payloadWithId envelope (the same shape estimateFees receives), + // not just the raw makePayload output. Reproduce that wrapping here so the byte count + // matches what the on-chain Relayer would check, then fail before propose is attempted. + const payloadWithIdSize = + ethers.utils.defaultAbiCoder.encode(["bytes", "uint256"], [remoteParam, 0]).length / 2 - 1; + if (payloadWithIdSize > LZ_PAYLOAD_SIZE_LIMIT) { + throw new Error( + `LayerZero payload size ${payloadWithIdSize} bytes exceeds limit of ${LZ_PAYLOAD_SIZE_LIMIT} ` + + `for dstChainId=${key} (${chainCommands.length} commands). ` + + `Split the proposal into multiple VIPs so each cross-chain message fits within the LZ Relayer cap.`, + ); + } + proposal.targets.push(getOmnichainProposalSenderAddress()); const value = await getEstimateFeesForBridge(key, remoteParam, remoteAdapterParam); proposal.values.push(value.toString()); From 9610faf70beadb23f7dfde847167cefe289f1d45 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 4 May 2026 18:20:16 +0530 Subject: [PATCH 14/15] feat(vip-616): configurator pattern to fit LZ payload + fix ACM bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cross-chain payload for VIP-616 + VIP-617 combined exceeds the LZ V1 RelayerV2 ~10 KB ceiling; encode all ACM grants and market wiring inside per-chain DeviationSentinelConfigurator so each chain needs only ~6 cross-chain commands instead of 100+ ACM grants - Merge vip-617 scope into vip-616 — configurator runs both bootstraps atomically in a single execute() call, no second VIP needed - Fix ACM bootstrap: VIP must grant DEFAULT_ADMIN_ROLE (not per-sig perms); Venus ACM.giveCallPermission wraps OZ grantRole which requires that role, per-sig grants caused execute() to revert on the first self-grant call - Add full fork simulation suite for Ethereum, Arbitrum One, and Base: pre/post ownership, ACM permissions, market wiring, and end-to-end SentinelOracle / checkPriceDeviation assertions --- simulations/vip-616/arbitrumone.ts | 2 +- simulations/vip-616/basemainnet.ts | 2 +- simulations/vip-616/ethereum.ts | 2 +- simulations/vip-616/shared.ts | 340 ++++++++++++++++++++++---- simulations/vip-617/arbitrumone.ts | 10 - simulations/vip-617/basemainnet.ts | 11 - simulations/vip-617/ethereum.ts | 11 - simulations/vip-617/shared.ts | 309 ----------------------- vips/vip-616/addresses/arbitrumone.ts | 7 +- vips/vip-616/addresses/basemainnet.ts | 7 +- vips/vip-616/addresses/ethereum.ts | 8 +- vips/vip-616/bscmainnet.ts | 152 +++++------- vips/vip-616/bsctestnet.ts | 14 +- vips/vip-617/bscmainnet.ts | 157 ------------ 14 files changed, 385 insertions(+), 647 deletions(-) delete mode 100644 simulations/vip-617/arbitrumone.ts delete mode 100644 simulations/vip-617/basemainnet.ts delete mode 100644 simulations/vip-617/ethereum.ts delete mode 100644 simulations/vip-617/shared.ts delete mode 100644 vips/vip-617/bscmainnet.ts diff --git a/simulations/vip-616/arbitrumone.ts b/simulations/vip-616/arbitrumone.ts index 6ca0b25c3..c95126e42 100644 --- a/simulations/vip-616/arbitrumone.ts +++ b/simulations/vip-616/arbitrumone.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { ARBITRUMONE_CONFIG } from "../../vips/vip-616/addresses/arbitrumone"; import { runVip616Suite } from "./shared"; -const FORK_BLOCK = 457580585; +const FORK_BLOCK = 457580585; // TODO: update to block after DeviationSentinelConfiguratorArbitrumOne deployment forking(FORK_BLOCK, async () => { await runVip616Suite(ARBITRUMONE_CONFIG); diff --git a/simulations/vip-616/basemainnet.ts b/simulations/vip-616/basemainnet.ts index eb8434ac8..89b917423 100644 --- a/simulations/vip-616/basemainnet.ts +++ b/simulations/vip-616/basemainnet.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { BASEMAINNET_CONFIG } from "../../vips/vip-616/addresses/basemainnet"; import { runVip616Suite } from "./shared"; -const FORK_BLOCK = 45338981; +const FORK_BLOCK = 45338981; // TODO: update to block after DeviationSentinelConfiguratorBaseMainnet deployment forking(FORK_BLOCK, async () => { await runVip616Suite(BASEMAINNET_CONFIG); diff --git a/simulations/vip-616/ethereum.ts b/simulations/vip-616/ethereum.ts index b526ebb28..d0af3e1e1 100644 --- a/simulations/vip-616/ethereum.ts +++ b/simulations/vip-616/ethereum.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { ETHEREUM_CONFIG } from "../../vips/vip-616/addresses/ethereum"; import { runVip616Suite } from "./shared"; -const FORK_BLOCK = 24992853; +const FORK_BLOCK = 24992853; // TODO: update to block after DeviationSentinelConfiguratorEthereum deployment forking(FORK_BLOCK, async () => { await runVip616Suite(ETHEREUM_CONFIG); diff --git a/simulations/vip-616/shared.ts b/simulations/vip-616/shared.ts index 88b22c66d..c73c1555a 100644 --- a/simulations/vip-616/shared.ts +++ b/simulations/vip-616/shared.ts @@ -3,16 +3,18 @@ import { expect } from "chai"; import { Contract } from "ethers"; import { ethers } from "hardhat"; import { ZERO_ADDRESS } from "src/networkAddresses"; -import { expectEvents, initMainnetUser } from "src/utils"; +import { expectEvents, getForkedNetworkAddress, initMainnetUser, setMaxStalePeriodInChainlinkOracle } from "src/utils"; import { testForkedNetworkVipCommands } from "src/vip-framework"; -import vip616, { +import vip616 from "../../vips/vip-616/bscmainnet"; +import { AERODROME_ORACLE_ADMIN_PERMS, CURVE_ORACLE_ADMIN_PERMS, ChainConfig, DIAMOND_ONLY_EBRAKE_PERMS, EBRAKE_COMPTROLLER_PERMS_IL, GOVERNANCE_EBRAKE_PERMS_IL, + MonitoredMarket, RESET_PERMS, SENTINEL_ADMIN_PERMS, SENTINEL_EBRAKE_PERMS, @@ -25,19 +27,55 @@ import AERODROME_ORACLE_ABI from "./abi/AerodromeSlipstreamOracle.json"; import CURVE_ORACLE_ABI from "./abi/CurveOracle.json"; import DEVIATION_SENTINEL_ABI from "./abi/DeviationSentinel.json"; import EBRAKE_ABI from "./abi/EBrake.json"; +import IL_COMPTROLLER_ABI from "./abi/ILComptroller.json"; +import RESILIENT_ORACLE_ABI from "./abi/ResilientOracle.json"; import SENTINEL_ORACLE_ABI from "./abi/SentinelOracle.json"; import UNISWAP_ORACLE_ABI from "./abi/UniswapOracle.json"; +import VTOKEN_ABI from "./abi/VToken.json"; + +// The VIP grants the configurator DEFAULT_ADMIN_ROLE on the local ACM so that +// execute() can call giveCallPermission / revokeCallPermission (both wrap OZ +// grantRole / revokeRole which require DEFAULT_ADMIN_ROLE). The configurator +// renounces this role at the end of execute() via _selfRevokeACMPermissions(). +const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; + +// Tokens whose `DeviationSentinel.checkPriceDeviation` end-to-end check is +// fragile at fork time only — wrapped/correlated oracles whose inner feeds +// expose staleness windows we can't override from the simulation side. +// - LBTC: OneJumpOracle (RedStone LBTC/BTC ratio + Chainlink BTC/USD). +// - eBTC: same OneJumpOracle pattern (eBTC/BTC ratio + BTC/USD). +const SKIP_CHECK_PRICE_DEVIATION = new Set(["LBTC", "eBTC"]); + +// RoleGranted event count emitted by VIP-616 per chain: +// 1 DEFAULT_ADMIN_ROLE grant (VIP-level grantRole(0x00, configurator)) +// + 51 base permission grants (12 sentinel admin + 8 sentinelOracle admin +// + 4 uniswapOracle admin + 4 ebrake→comptroller +// + 12 reset + 3 sentinel→ebrake + 8 multisig) +// + 4 if cfg.curveOracle present (CurveOracle admin × 4 govAccounts) +// + 4 if cfg.aerodromeOracle present (AerodromeSlipstreamOracle admin × 4 govAccounts) +// + 32 governance EBrake-action grants (8 sigs × 4 govAccounts) +// + 4 transient self-grants in execute() (_selfGrantBaseTransientPermissions: +// DeviationSentinel×2, SentinelOracle×1, UniswapOracle×1) +// + 1 if cfg.curveOracle / aerodromeOracle present (_selfGrantChainSpecificTransientPermissions) +const expectedRoleGranted = (cfg: ChainConfig): number => { + let n = 1 + 51 + 32 + 4; // DEFAULT_ADMIN_ROLE + base grants + governance EBrake + base transient + if (cfg.curveOracle) n += 4 + 1; // CurveOracle admin grants + transient self-grant + if (cfg.aerodromeOracle) n += 4 + 1; // AerodromeOracle admin grants + transient self-grant + return n; +}; -// RoleGranted events emitted by VIP-616 (Sub-A). Per-chain count is variable because -// CurveOracle (Ethereum) and AerodromeSlipstreamOracle (Base) each add 4 admin grants. -// base = admin grants (12+8+4) + ebrake→comptroller (4) + reset (12) + sentinel→ebrake (3) + multisig (8) = 51 -// +4 if cfg.curveOracle is set, +4 if cfg.aerodromeOracle is set -const expectedPermsGranted = (cfg: ChainConfig): number => { - let n = 51; - if (cfg.curveOracle) n += 4; - if (cfg.aerodromeOracle) n += 4; +// RoleRevoked event count emitted by VIP-616 per chain: +// 4 base transient self-revokes in execute() (_selfRevokeBaseTransientPermissions) +// + 1 if cfg.curveOracle / aerodromeOracle present (_selfRevokeChainSpecificTransientPermissions) +// + 1 configurator renounces DEFAULT_ADMIN_ROLE (_selfRevokeACMPermissions) +const expectedRoleRevoked = (cfg: ChainConfig): number => { + let n = 4 + 1; // base transient revokes + DEFAULT_ADMIN_ROLE renounce + if (cfg.curveOracle || cfg.aerodromeOracle) n += 1; return n; }; + +// OwnershipTransferred count = 4 base contracts (DeviationSentinel, SentinelOracle, +// UniswapOracle, EBrake) + 1 each for CurveOracle / AerodromeSlipstreamOracle. const expectedAcceptOwnership = (cfg: ChainConfig): number => { let n = 4; if (cfg.curveOracle) n += 1; @@ -45,6 +83,59 @@ const expectedAcceptOwnership = (cfg: ChainConfig): number => { return n; }; +const countMarketsByOracle = (markets: MonitoredMarket[], type: MonitoredMarket["oracleType"]) => + markets.filter(m => (m.oracleType ?? "uniswap") === (type ?? "uniswap")).length; + +const dexOracleFor = (cfg: ChainConfig, market: MonitoredMarket): string => { + switch (market.oracleType ?? "uniswap") { + case "uniswap": + return cfg.uniswapOracle; + case "curve": + return cfg.curveOracle as string; + case "aerodrome": + return cfg.aerodromeOracle as string; + } +}; + +const tryGetAddress = (key: string): string | undefined => { + try { + return getForkedNetworkAddress(key); + } catch { + return undefined; + } +}; + +// testForkedNetworkVipCommands advances chain time past timelock delays. Past +// that window Chainlink/RedStone heartbeats expire and ResilientOracle reverts +// with "invalid resilient oracle price". Bump at the adapter level so main + +// pivot + fallback slots and OneJumpOracle wrappers are all covered. +const bumpAdapterStaleness = async (cfg: ChainConfig) => { + const adapters = [tryGetAddress("CHAINLINK_ORACLE"), tryGetAddress("REDSTONE_ORACLE")].filter( + (a): a is string => !!a, + ); + for (const market of cfg.monitoredMarkets) { + for (const adapter of adapters) { + await setMaxStalePeriodInChainlinkOracle(adapter, market.token, ZERO_ADDRESS, cfg.normalTimelock); + } + } +}; + +const buildVTokenIndex = async (comptrollerAddress: string): Promise> => { + const comptroller = await ethers.getContractAt(IL_COMPTROLLER_ABI, comptrollerAddress); + const vTokens: string[] = await comptroller.getAllMarkets(); + const vTokenByUnderlying = new Map(); + for (const vToken of vTokens) { + try { + const v = await ethers.getContractAt(VTOKEN_ABI, vToken); + const underlying: string = await v.underlying(); + vTokenByUnderlying.set(underlying.toLowerCase(), vToken); + } catch { + // native-token vTokens don't expose underlying() — skip + } + } + return vTokenByUnderlying; +}; + const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { const missing: string[] = []; if (cfg.deviationSentinel === ZERO_ADDRESS) missing.push("deviationSentinel"); @@ -53,6 +144,7 @@ const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { if (cfg.uniswapOracle === ZERO_ADDRESS) missing.push("uniswapOracle"); if (cfg.multisigPauser === ZERO_ADDRESS) missing.push("multisigPauser"); if (cfg.keeper === ZERO_ADDRESS) missing.push("keeper"); + if (cfg.configurator === ZERO_ADDRESS) missing.push("configurator"); return missing; }; @@ -60,9 +152,22 @@ export const runVip616Suite = async (cfg: ChainConfig) => { const missing = collectMissingPlaceholders(cfg); if (missing.length > 0) { describe.skip(`VIP-616 [${cfg.name}] — placeholder addresses missing: ${missing.join(", ")}`, () => { - it(`Fill ${missing.join(", ")} in addresses/${cfg.name - .toLowerCase() - .replace(/\s/g, "")}.ts to run this suite`, () => { + it(`Fill ${missing.join(", ")} to run this suite`, () => { + // intentionally empty — skip stub + }); + }); + return; + } + + // vip616() iterates all three chains and throws if any configurator is ZERO_ADDRESS. + // Catch so one undeployed chain doesn't silently break another chain's sim file. + let proposal: Awaited>; + try { + proposal = await vip616(); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + describe.skip(`VIP-616 [${cfg.name}] — cannot build VIP (all configurators must be set): ${msg}`, () => { + it("Deploy all three configurators and update addresses/.ts", () => { // intentionally empty — skip stub }); }); @@ -76,6 +181,9 @@ export const runVip616Suite = async (cfg: ChainConfig) => { let uniswapOracle: Contract; let curveOracle: Contract | undefined; let aerodromeOracle: Contract | undefined; + let resilientOracle: Contract; + // underlying address (lowercased) → vToken address — built by walking IL Comptroller markets. + let vTokenByUnderlying: Map; // ACM.isAllowedToCall(account, sig) checks msg.sender as the host contract. // Impersonate each host so the role lookup resolves correctly. @@ -95,6 +203,7 @@ export const runVip616Suite = async (cfg: ChainConfig) => { eBrake = await ethers.getContractAt(EBRAKE_ABI, cfg.eBrake); sentinelOracle = await ethers.getContractAt(SENTINEL_ORACLE_ABI, cfg.sentinelOracle); uniswapOracle = await ethers.getContractAt(UNISWAP_ORACLE_ABI, cfg.uniswapOracle); + resilientOracle = await ethers.getContractAt(RESILIENT_ORACLE_ABI, getForkedNetworkAddress("RESILIENT_ORACLE")); impersonatedDeviationSentinel = await initMainnetUser(cfg.deviationSentinel, ethers.utils.parseEther("1")); impersonatedSentinelOracle = await initMainnetUser(cfg.sentinelOracle, ethers.utils.parseEther("1")); @@ -109,8 +218,38 @@ export const runVip616Suite = async (cfg: ChainConfig) => { aerodromeOracle = await ethers.getContractAt(AERODROME_ORACLE_ABI, cfg.aerodromeOracle); impersonatedAerodromeOracle = await initMainnetUser(cfg.aerodromeOracle, ethers.utils.parseEther("1")); } + + await bumpAdapterStaleness(cfg); + vTokenByUnderlying = await buildVTokenIndex(cfg.comptroller); + }); + + // -------------------- Monitored-market config validity -------------------- + describe(`VIP-616 [${cfg.name}] — Monitored markets config validity`, () => { + // A zero-address token or pool, or an out-of-range deviation, would be silently + // skipped or accepted-as-malformed by the wiring loop. Fail loud at simulation time. + it("Every monitored market has non-zero token, non-zero pool, and 0 < deviation ≤ 100", () => { + for (const market of cfg.monitoredMarkets) { + expect(market.token, `${market.symbol}: token is ZERO_ADDRESS`).to.not.equal(ZERO_ADDRESS); + expect(market.pool, `${market.symbol}: pool is ZERO_ADDRESS`).to.not.equal(ZERO_ADDRESS); + expect(market.token.length, `${market.symbol}: token not 20 bytes`).to.equal(42); + expect(market.pool.length, `${market.symbol}: pool not 20 bytes`).to.equal(42); + expect(market.deviationPercent, `${market.symbol}: deviation must be > 0`).to.be.greaterThan(0); + expect(market.deviationPercent, `${market.symbol}: deviation must be ≤ 100`).to.be.lessThanOrEqual(100); + } + }); + + // ResilientOracle is the reference price source for handleDeviation. If it has + // no feed for a monitored token, oraclePrice = 0 makes hasDeviation always true + // and every keeper call would pause the market. + it("ResilientOracle returns a non-zero price for every monitored token", async () => { + for (const market of cfg.monitoredMarkets) { + const price = await resilientOracle.getPrice(market.token); + expect(price, `${market.symbol}: ResilientOracle.getPrice returned 0`).to.be.gt(0); + } + }); }); + // -------------------- Pre-VIP behaviour -------------------- describe(`VIP-616 [${cfg.name}] — Pre-VIP behaviour`, () => { it("DeviationSentinel pendingOwner is Normal Timelock", async () => { expect(await deviationSentinel.pendingOwner()).to.equal(cfg.normalTimelock); @@ -150,6 +289,10 @@ export const runVip616Suite = async (cfg: ChainConfig) => { expect(await deviationSentinel.SENTINEL_ORACLE()).to.equal(cfg.sentinelOracle); }); + it("Configurator does not yet hold DEFAULT_ADMIN_ROLE on ACM", async () => { + expect(await acm.hasRole(DEFAULT_ADMIN_ROLE, cfg.configurator)).to.equal(false); + }); + it("Guardian + Timelocks have no admin permissions on DeviationSentinel yet", async () => { const a = acm.connect(impersonatedDeviationSentinel); for (const account of govAccounts) { @@ -199,10 +342,13 @@ export const runVip616Suite = async (cfg: ChainConfig) => { }); } - it("EBrake has no permissions on the IL Comptroller yet", async () => { - const a = acm.connect(impersonatedComptroller); + // Use hasPermission (specific role lookup), not isAllowedToCall — the IL Comptroller is + // a live contract that may already hold wildcard grants on these sigs (Risk Steward, + // Guardian, etc.), and isAllowedToCall would return true off the wildcard, masking the + // genuine pre-VIP state of EBrake's specific perm. + it("EBrake has no specific permissions on the IL Comptroller yet", async () => { for (const sig of EBRAKE_COMPTROLLER_PERMS_IL) { - expect(await a.isAllowedToCall(cfg.eBrake, sig)).to.equal(false, `unexpected ${sig}`); + expect(await acm.hasPermission(cfg.eBrake, cfg.comptroller, sig)).to.equal(false, `unexpected ${sig}`); } }); @@ -226,30 +372,86 @@ export const runVip616Suite = async (cfg: ChainConfig) => { } }); + it("Guardian + Timelocks have no EBrake-specific action permissions yet", async () => { + for (const account of govAccounts) { + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(false, `unexpected ${sig} for ${account}`); + } + } + }); + it("No accounts are whitelisted as trusted keepers yet", async () => { for (const account of trustedKeeperAccounts) { expect(await deviationSentinel.trustedKeepers(account)).to.equal(false); } }); + + for (const market of cfg.monitoredMarkets) { + it(`${market.symbol} is not wired yet`, async () => { + // Each oracle uses a different storage shape for its pool config: + // - UniswapOracle / AerodromeSlipstreamOracle: tokenPools(token) -> address + // - CurveOracle: poolConfigs(token) -> { pool, coinIndex, referenceToken, ... } + const oracleType = market.oracleType ?? "uniswap"; + if (oracleType === "curve") { + const cfgEntry = await curveOracle!.poolConfigs(market.token); + expect(cfgEntry.pool).to.equal(ZERO_ADDRESS); + } else if (oracleType === "aerodrome") { + expect(await aerodromeOracle!.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + } else { + expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + } + const tcSentinel = await sentinelOracle.tokenConfigs(market.token); + expect(tcSentinel.oracle ?? tcSentinel).to.equal(ZERO_ADDRESS); + const tcDev = await deviationSentinel.tokenConfigs(market.token); + expect(tcDev.deviation).to.equal(0); + expect(tcDev.enabled).to.equal(false); + }); + } }); - testForkedNetworkVipCommands(`VIP-616 [${cfg.name}] Bootstrap & Permissions`, await vip616(), { + // -------------------- Apply VIP -------------------- + testForkedNetworkVipCommands(`VIP-616 [${cfg.name}] Bootstrap, Permissions, Wiring`, proposal, { callbackAfterExecution: async txResponse => { - // 4 + (1 each for CurveOracle / AerodromeSlipstreamOracle) acceptOwnership() calls + // 4 acceptOwnership() calls, +1 each for CurveOracle / AerodromeSlipstreamOracle. await expectEvents( txResponse, [DEVIATION_SENTINEL_ABI], ["OwnershipTransferred"], [expectedAcceptOwnership(cfg)], ); - // 5 trusted keepers whitelisted per chain + + // 5 trusted keepers per chain: cfg.keeper + 4 governance accounts. await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TrustedKeeperUpdated"], [5]); - // RoleGranted events (Sub-A only — governance EBrake actions deferred to VIP-617). - // Variable per chain depending on which optional DEX oracles are present. - await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [expectedPermsGranted(cfg)]); + + // Total RoleGranted per chain — see expectedRoleGranted comment for the breakdown. + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [expectedRoleGranted(cfg)]); + + // Configurator revokes its transient periphery perms and renounces DEFAULT_ADMIN_ROLE + // at the end of execute() — see expectedRoleRevoked comment for the breakdown. + await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleRevoked"], [expectedRoleRevoked(cfg)]); + + // Pool wiring counts. UniswapOracle and AerodromeSlipstreamOracle emit the same + // 2-arg `PoolConfigUpdated(address,address)` event (identical topic hash), so + // the UniswapOracle ABI filter decodes both. CurveOracle's 4-arg variant has its + // own topic and is asserted separately. + const uniswapMarkets = countMarketsByOracle(cfg.monitoredMarkets, "uniswap"); + const curveMarkets = countMarketsByOracle(cfg.monitoredMarkets, "curve"); + const aerodromeMarkets = countMarketsByOracle(cfg.monitoredMarkets, "aerodrome"); + await expectEvents(txResponse, [UNISWAP_ORACLE_ABI], ["PoolConfigUpdated"], [uniswapMarkets + aerodromeMarkets]); + if (cfg.curveOracle && curveMarkets > 0) { + await expectEvents(txResponse, [CURVE_ORACLE_ABI], ["PoolConfigUpdated"], [curveMarkets]); + } + await expectEvents( + txResponse, + [SENTINEL_ORACLE_ABI], + ["TokenOracleConfigUpdated"], + [cfg.monitoredMarkets.length], + ); + await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TokenConfigUpdated"], [cfg.monitoredMarkets.length]); }, }); + // -------------------- Post-VIP behaviour -------------------- describe(`VIP-616 [${cfg.name}] — Post-VIP behaviour`, () => { it("All bootstrap contracts have Normal Timelock as owner", async () => { expect(await deviationSentinel.owner()).to.equal(cfg.normalTimelock); @@ -269,6 +471,16 @@ export const runVip616Suite = async (cfg: ChainConfig) => { if (cfg.aerodromeOracle) expect(await aerodromeOracle!.pendingOwner()).to.equal(ZERO_ADDRESS); }); + // The configurator renounces DEFAULT_ADMIN_ROLE at the end of execute() via + // _selfRevokeACMPermissions(). Leaving it in place would be a standing + // ACM-mutation foothold. + it("Configurator no longer holds DEFAULT_ADMIN_ROLE on ACM (renounced in execute())", async () => { + expect(await acm.hasRole(DEFAULT_ADMIN_ROLE, cfg.configurator)).to.equal( + false, + "configurator still holds DEFAULT_ADMIN_ROLE", + ); + }); + it("Guardian + Timelocks have all admin permissions on DeviationSentinel", async () => { const a = acm.connect(impersonatedDeviationSentinel); for (const account of govAccounts) { @@ -345,6 +557,14 @@ export const runVip616Suite = async (cfg: ChainConfig) => { } }); + it("Guardian + Timelocks have all 8 IL-supported EBrake action permissions", async () => { + for (const account of govAccounts) { + for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { + expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(true, `missing ${sig} for ${account}`); + } + } + }); + it("Diamond-only EBrake permissions are intentionally NOT granted", async () => { for (const account of govAccounts) { for (const sig of DIAMOND_ONLY_EBRAKE_PERMS) { @@ -362,38 +582,66 @@ export const runVip616Suite = async (cfg: ChainConfig) => { } }); - // Sub-A intentionally does NOT grant governance EBrake action perms or wire markets - // — those land in VIP-617. Assert the deferred state explicitly so a regression is loud. - it("Guardian + Timelocks still have no EBrake-specific action permissions (deferred to VIP-617)", async () => { - for (const account of govAccounts) { - for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { - expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal( - false, - `unexpected ${sig} for ${account} — should be granted by VIP-617`, - ); - } - } - }); - for (const market of cfg.monitoredMarkets) { - if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; - it(`${market.symbol} is still not wired (deferred to VIP-617)`, async () => { - // Each market's wiring lives on its routed DEX oracle; assert its slot is empty. + const expectedDexOracle = dexOracleFor(cfg, market); + + it(`${market.symbol} pool is configured on the routed DEX oracle`, async () => { const oracleType = market.oracleType ?? "uniswap"; if (oracleType === "curve") { const cfgEntry = await curveOracle!.poolConfigs(market.token); - expect(cfgEntry.pool).to.equal(ZERO_ADDRESS); + expect(ethers.utils.getAddress(cfgEntry.pool)).to.equal(ethers.utils.getAddress(market.pool)); + expect(cfgEntry.coinIndex).to.equal(market.coinIndex); + expect(cfgEntry.refCoinIndex).to.equal(market.refCoinIndex); + expect(ethers.utils.getAddress(cfgEntry.referenceToken)).to.equal( + ethers.utils.getAddress(market.referenceToken as string), + ); + expect(cfgEntry.assetDecimals).to.equal(market.assetDecimals); } else if (oracleType === "aerodrome") { - expect(await aerodromeOracle!.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + const actual = await aerodromeOracle!.tokenPools(market.token); + expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); } else { - expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); + const actual = await uniswapOracle.tokenPools(market.token); + expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); } - const tcSentinel = await sentinelOracle.tokenConfigs(market.token); - expect(tcSentinel.oracle ?? tcSentinel).to.equal(ZERO_ADDRESS); - const tcDev = await deviationSentinel.tokenConfigs(market.token); - expect(tcDev.deviation).to.equal(0); - expect(tcDev.enabled).to.equal(false); }); + + it(`${market.symbol} oracle is configured on SentinelOracle`, async () => { + const tc = await sentinelOracle.tokenConfigs(market.token); + const actual = tc.oracle ?? tc; + expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(expectedDexOracle)); + }); + + it(`${market.symbol} deviation threshold is configured on DeviationSentinel`, async () => { + const tc = await deviationSentinel.tokenConfigs(market.token); + expect(tc.deviation).to.equal(market.deviationPercent); + expect(tc.enabled).to.equal(true); + }); + + // End-to-end price-pipeline check: SentinelOracle → routed DEX oracle → pool. + // If the pool isn't a real Uniswap V3 / V3-compatible contract, slot0 reverts + // and handleDeviation would always revert (silent monitoring outage). + it(`${market.symbol} SentinelOracle.getPrice returns a non-zero price`, async () => { + const price = await sentinelOracle.getPrice(market.token); + expect(price, `${market.symbol}: SentinelOracle.getPrice returned 0`).to.be.gt(0); + }); + + // Full handleDeviation pre-flight: same path the keeper exercises, minus the + // EBrake side-effect. Skipped for fork-fragile tokens (see SKIP_CHECK_PRICE_DEVIATION). + if (!SKIP_CHECK_PRICE_DEVIATION.has(market.symbol)) { + it(`${market.symbol} DeviationSentinel.checkPriceDeviation returns hasDeviation=false at fork time`, async () => { + const vToken = vTokenByUnderlying.get(market.token.toLowerCase()); + expect(vToken, `${market.symbol}: no vToken in Core Pool with underlying=${market.token}`).to.not.be + .undefined; + const [hasDeviation, oraclePrice, sentinelPrice, deviationPercent] = + await deviationSentinel.checkPriceDeviation(vToken); + expect(oraclePrice, `${market.symbol}: oraclePrice = 0`).to.be.gt(0); + expect(sentinelPrice, `${market.symbol}: sentinelPrice = 0`).to.be.gt(0); + expect(deviationPercent, `${market.symbol}: live deviation ≥ trigger threshold`).to.be.lt( + market.deviationPercent, + ); + expect(hasDeviation, `${market.symbol}: handleDeviation would trigger right now`).to.equal(false); + }); + } } }); }; diff --git a/simulations/vip-617/arbitrumone.ts b/simulations/vip-617/arbitrumone.ts deleted file mode 100644 index 21b259372..000000000 --- a/simulations/vip-617/arbitrumone.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { forking } from "src/vip-framework"; - -import { ARBITRUMONE_CONFIG } from "../../vips/vip-616/addresses/arbitrumone"; -import { runVip617Suite } from "./shared"; - -const FORK_BLOCK = 457434103; - -forking(FORK_BLOCK, async () => { - await runVip617Suite(ARBITRUMONE_CONFIG); -}); diff --git a/simulations/vip-617/basemainnet.ts b/simulations/vip-617/basemainnet.ts deleted file mode 100644 index 147120495..000000000 --- a/simulations/vip-617/basemainnet.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { forking } from "src/vip-framework"; - -import { BASEMAINNET_CONFIG } from "../../vips/vip-616/addresses/basemainnet"; -import { runVip617Suite } from "./shared"; - -// Must be ≥ 45337626 — AerodromeSlipstreamOracle proxy deployment block. -const FORK_BLOCK = 45338981; - -forking(FORK_BLOCK, async () => { - await runVip617Suite(BASEMAINNET_CONFIG); -}); diff --git a/simulations/vip-617/ethereum.ts b/simulations/vip-617/ethereum.ts deleted file mode 100644 index 252f7e033..000000000 --- a/simulations/vip-617/ethereum.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { forking } from "src/vip-framework"; - -import { ETHEREUM_CONFIG } from "../../vips/vip-616/addresses/ethereum"; -import { runVip617Suite } from "./shared"; - -// Must be ≥ 24985630 — CurveOracle proxy deployment block. -const FORK_BLOCK = 24992834; - -forking(FORK_BLOCK, async () => { - await runVip617Suite(ETHEREUM_CONFIG); -}); diff --git a/simulations/vip-617/shared.ts b/simulations/vip-617/shared.ts deleted file mode 100644 index cfede0dfd..000000000 --- a/simulations/vip-617/shared.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { expect } from "chai"; -import { Contract } from "ethers"; -import { ethers } from "hardhat"; -import { ZERO_ADDRESS } from "src/networkAddresses"; -import { expectEvents, getForkedNetworkAddress, setMaxStalePeriodInChainlinkOracle } from "src/utils"; -import { testForkedNetworkVipCommands } from "src/vip-framework"; - -import vip616, { - ChainConfig, - GOVERNANCE_EBRAKE_PERMS_IL, - MonitoredMarket, - governanceAccounts, -} from "../../vips/vip-616/bscmainnet"; -import vip617 from "../../vips/vip-617/bscmainnet"; -import ACCESS_CONTROL_MANAGER_ABI from "../vip-616/abi/AccessControlManager.json"; -import AERODROME_ORACLE_ABI from "../vip-616/abi/AerodromeSlipstreamOracle.json"; -import CURVE_ORACLE_ABI from "../vip-616/abi/CurveOracle.json"; -import DEVIATION_SENTINEL_ABI from "../vip-616/abi/DeviationSentinel.json"; -import IL_COMPTROLLER_ABI from "../vip-616/abi/ILComptroller.json"; -import RESILIENT_ORACLE_ABI from "../vip-616/abi/ResilientOracle.json"; -import SENTINEL_ORACLE_ABI from "../vip-616/abi/SentinelOracle.json"; -import UNISWAP_ORACLE_ABI from "../vip-616/abi/UniswapOracle.json"; -import VTOKEN_ABI from "../vip-616/abi/VToken.json"; - -// RoleGranted events emitted by VIP-617 (Sub-B) per chain: -// 8 (governance ebrake action) × 4 = 32 (token wiring uses direct setter calls) -const PERMS_GRANTED_PER_CHAIN = 32; - -// Resolve the DEX-oracle address used to price a market based on its oracleType. -// Mirrors `resolveDexOracle` in the VIP itself — kept inline (not imported) because -// the VIP module throws on misconfig and the simulation's expressed intent is checking -// post-state, not duplicating proposal-build validation. -const dexOracleFor = (cfg: ChainConfig, market: MonitoredMarket): string => { - switch (market.oracleType ?? "uniswap") { - case "uniswap": - return cfg.uniswapOracle; - case "curve": - return cfg.curveOracle as string; - case "aerodrome": - return cfg.aerodromeOracle as string; - } -}; - -const countMarketsByOracle = (markets: MonitoredMarket[], type: MonitoredMarket["oracleType"]) => - markets.filter(m => (m.oracleType ?? "uniswap") === (type ?? "uniswap")).length; - -// Tokens whose `DeviationSentinel.checkPriceDeviation` end-to-end check is fragile at -// fork time only — typically wrapped/correlated oracles whose inner feeds expose -// staleness windows we can't override from the simulation side. Production behaviour -// is unaffected (verified at live timestamps). -// - LBTC: main oracle is OneJumpOracle (RedStone LBTC/BTC ratio + Chainlink BTC/USD). -// - eBTC: same OneJumpOracle pattern (eBTC/BTC ratio + BTC/USD), so matches LBTC's profile. -const SKIP_CHECK_PRICE_DEVIATION = new Set(["LBTC", "eBTC"]); - -const collectMissingPlaceholders = (cfg: ChainConfig): string[] => { - const missing: string[] = []; - if (cfg.deviationSentinel === ZERO_ADDRESS) missing.push("deviationSentinel"); - if (cfg.eBrake === ZERO_ADDRESS) missing.push("eBrake"); - if (cfg.sentinelOracle === ZERO_ADDRESS) missing.push("sentinelOracle"); - if (cfg.uniswapOracle === ZERO_ADDRESS) missing.push("uniswapOracle"); - if (cfg.multisigPauser === ZERO_ADDRESS) missing.push("multisigPauser"); - if (cfg.keeper === ZERO_ADDRESS) missing.push("keeper"); - return missing; -}; - -// testForkedNetworkVipCommands advances chain time past timelock delays. Past that -// window Chainlink/RedStone heartbeats expire and ResilientOracle._getPrice reverts -// with "invalid resilient oracle price". Bumping at the adapter level (rather than via -// ResilientOracle's main slot) covers main + pivot + fallback slots and any wrapper -// oracles (e.g. OneJumpOracle) that internally read from these adapters. -// setMaxStalePeriodInChainlinkOracle no-ops when an adapter has no feed for the asset. -const tryGetAddress = (key: string): string | undefined => { - try { - return getForkedNetworkAddress(key); - } catch { - return undefined; - } -}; - -const bumpAdapterStaleness = async (cfg: ChainConfig) => { - const adapters = [tryGetAddress("CHAINLINK_ORACLE"), tryGetAddress("REDSTONE_ORACLE")].filter( - (a): a is string => !!a, - ); - for (const market of cfg.monitoredMarkets) { - for (const adapter of adapters) { - await setMaxStalePeriodInChainlinkOracle(adapter, market.token, ZERO_ADDRESS, cfg.normalTimelock); - } - } -}; - -// Walk the IL Comptroller's markets and build an underlying-address → vToken-address -// lookup. Used by the post-VIP `checkPriceDeviation` test, which needs the vToken to -// hand to DeviationSentinel.checkPriceDeviation. Skips native-token vTokens that -// don't expose underlying() (the try/catch prevents the call from blowing up). -const buildVTokenIndex = async (comptrollerAddress: string): Promise> => { - const comptroller = await ethers.getContractAt(IL_COMPTROLLER_ABI, comptrollerAddress); - const vTokens: string[] = await comptroller.getAllMarkets(); - const vTokenByUnderlying = new Map(); - for (const vToken of vTokens) { - try { - const v = await ethers.getContractAt(VTOKEN_ABI, vToken); - const underlying: string = await v.underlying(); - vTokenByUnderlying.set(underlying.toLowerCase(), vToken); - } catch { - // native-token vTokens don't expose underlying() — skip - } - } - return vTokenByUnderlying; -}; - -export const runVip617Suite = async (cfg: ChainConfig) => { - const missing = collectMissingPlaceholders(cfg); - if (missing.length > 0) { - describe.skip(`VIP-617 [${cfg.name}] — placeholder addresses missing: ${missing.join(", ")}`, () => { - it(`Fill ${missing.join(", ")} in vips/vip-616/addresses/${cfg.name - .toLowerCase() - .replace(/\s/g, "")}.ts to run this suite`, () => { - // intentionally empty — skip stub - }); - }); - return; - } - - let acm: Contract; - let deviationSentinel: Contract; - let sentinelOracle: Contract; - let uniswapOracle: Contract; - let curveOracle: Contract | undefined; - let aerodromeOracle: Contract | undefined; - let resilientOracle: Contract; - // underlying address (lowercased) → vToken address — built by walking IL Comptroller markets. - let vTokenByUnderlying: Map; - - const govAccounts = governanceAccounts(cfg); - - before(async () => { - acm = await ethers.getContractAt(ACCESS_CONTROL_MANAGER_ABI, cfg.acm); - deviationSentinel = await ethers.getContractAt(DEVIATION_SENTINEL_ABI, cfg.deviationSentinel); - sentinelOracle = await ethers.getContractAt(SENTINEL_ORACLE_ABI, cfg.sentinelOracle); - uniswapOracle = await ethers.getContractAt(UNISWAP_ORACLE_ABI, cfg.uniswapOracle); - if (cfg.curveOracle) curveOracle = await ethers.getContractAt(CURVE_ORACLE_ABI, cfg.curveOracle); - if (cfg.aerodromeOracle) aerodromeOracle = await ethers.getContractAt(AERODROME_ORACLE_ABI, cfg.aerodromeOracle); - resilientOracle = await ethers.getContractAt(RESILIENT_ORACLE_ABI, getForkedNetworkAddress("RESILIENT_ORACLE")); - - await bumpAdapterStaleness(cfg); - vTokenByUnderlying = await buildVTokenIndex(cfg.comptroller); - }); - - describe(`VIP-617 [${cfg.name}] — Monitored markets config validity`, () => { - // A zero-address token or pool, or an out-of-range deviation, would be silently - // skipped or accepted-as-malformed by the wiring loop. Fail loud at simulation time. - it("Every monitored market has non-zero token, non-zero pool, and 0 < deviation ≤ 100", () => { - for (const market of cfg.monitoredMarkets) { - expect(market.token, `${market.symbol}: token is ZERO_ADDRESS`).to.not.equal(ZERO_ADDRESS); - expect(market.pool, `${market.symbol}: pool is ZERO_ADDRESS`).to.not.equal(ZERO_ADDRESS); - expect(market.token.length, `${market.symbol}: token not 20 bytes`).to.equal(42); - expect(market.pool.length, `${market.symbol}: pool not 20 bytes`).to.equal(42); - expect(market.deviationPercent, `${market.symbol}: deviation must be > 0`).to.be.greaterThan(0); - expect(market.deviationPercent, `${market.symbol}: deviation must be ≤ 100`).to.be.lessThanOrEqual(100); - } - }); - - // ResilientOracle is the reference price source for handleDeviation. If it has no - // feed for a monitored token, oraclePrice = 0 makes hasDeviation always true and - // every keeper call would pause the market. - it("ResilientOracle returns a non-zero price for every monitored token", async () => { - for (const market of cfg.monitoredMarkets) { - const price = await resilientOracle.getPrice(market.token); - expect(price, `${market.symbol}: ResilientOracle.getPrice returned 0`).to.be.gt(0); - } - }); - }); - - // VIP-617 depends on VIP-616 having been executed — apply it first within the - // same fork so post-A state is the pre-VIP state for B. - testForkedNetworkVipCommands(`VIP-616 [${cfg.name}] (prerequisite for VIP-617)`, await vip616()); - - describe(`VIP-617 [${cfg.name}] — Pre-VIP behaviour (post-VIP-616 state)`, () => { - it("Guardian + Timelocks have no EBrake-specific action permissions yet", async () => { - for (const account of govAccounts) { - for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { - expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(false, `unexpected ${sig} for ${account}`); - } - } - }); - - for (const market of cfg.monitoredMarkets) { - it(`${market.symbol} is not wired yet`, async () => { - // Pre-VIP, the appropriate DEX oracle has no pool entry for this token. - // Each oracle uses a different storage shape for its pool config: - // - UniswapOracle / AerodromeSlipstreamOracle: tokenPools(token) -> address - // - CurveOracle: poolConfigs(token) -> { pool, coinIndex, referenceToken } - const oracleType = market.oracleType ?? "uniswap"; - if (oracleType === "curve") { - const cfgEntry = await curveOracle!.poolConfigs(market.token); - expect(cfgEntry.pool).to.equal(ZERO_ADDRESS); - } else if (oracleType === "aerodrome") { - expect(await aerodromeOracle!.tokenPools(market.token)).to.equal(ZERO_ADDRESS); - } else { - expect(await uniswapOracle.tokenPools(market.token)).to.equal(ZERO_ADDRESS); - } - const tcSentinel = await sentinelOracle.tokenConfigs(market.token); - expect(tcSentinel.oracle ?? tcSentinel).to.equal(ZERO_ADDRESS); - const tcDev = await deviationSentinel.tokenConfigs(market.token); - expect(tcDev.deviation).to.equal(0); - expect(tcDev.enabled).to.equal(false); - }); - } - }); - - testForkedNetworkVipCommands(`VIP-617 [${cfg.name}] Governance Actions & Market Wiring`, await vip617(), { - callbackAfterExecution: async txResponse => { - // 32 RoleGranted events per chain (governance EBrake action perms) - await expectEvents(txResponse, [ACCESS_CONTROL_MANAGER_ABI], ["RoleGranted"], [PERMS_GRANTED_PER_CHAIN]); - // PoolConfigUpdated counts. UniswapOracle and AerodromeSlipstreamOracle emit the - // identical 2-arg `PoolConfigUpdated(address,address)` event (same topic hash), - // so a UniswapOracle-ABI filter decodes both — assert against the combined count. - // CurveOracle emits its own 4-arg variant (distinct topic), checked separately. - const uniswapMarkets = countMarketsByOracle(cfg.monitoredMarkets, "uniswap"); - const curveMarkets = countMarketsByOracle(cfg.monitoredMarkets, "curve"); - const aerodromeMarkets = countMarketsByOracle(cfg.monitoredMarkets, "aerodrome"); - await expectEvents(txResponse, [UNISWAP_ORACLE_ABI], ["PoolConfigUpdated"], [uniswapMarkets + aerodromeMarkets]); - if (cfg.curveOracle && curveMarkets > 0) { - await expectEvents(txResponse, [CURVE_ORACLE_ABI], ["PoolConfigUpdated"], [curveMarkets]); - } - await expectEvents( - txResponse, - [SENTINEL_ORACLE_ABI], - ["TokenOracleConfigUpdated"], - [cfg.monitoredMarkets.length], - ); - await expectEvents(txResponse, [DEVIATION_SENTINEL_ABI], ["TokenConfigUpdated"], [cfg.monitoredMarkets.length]); - }, - }); - - describe(`VIP-617 [${cfg.name}] — Post-VIP behaviour`, () => { - it("Guardian + Timelocks have all 8 IL-supported EBrake action permissions", async () => { - for (const account of govAccounts) { - for (const sig of GOVERNANCE_EBRAKE_PERMS_IL) { - expect(await acm.hasPermission(account, cfg.eBrake, sig)).to.equal(true, `missing ${sig} for ${account}`); - } - } - }); - - for (const market of cfg.monitoredMarkets) { - const expectedDexOracle = dexOracleFor(cfg, market); - - it(`${market.symbol} pool is configured on the routed DEX oracle`, async () => { - const oracleType = market.oracleType ?? "uniswap"; - if (oracleType === "curve") { - const cfgEntry = await curveOracle!.poolConfigs(market.token); - expect(ethers.utils.getAddress(cfgEntry.pool)).to.equal(ethers.utils.getAddress(market.pool)); - expect(cfgEntry.coinIndex).to.equal(market.coinIndex); - expect(cfgEntry.refCoinIndex).to.equal(market.refCoinIndex); - expect(ethers.utils.getAddress(cfgEntry.referenceToken)).to.equal( - ethers.utils.getAddress(market.referenceToken as string), - ); - expect(cfgEntry.assetDecimals).to.equal(market.assetDecimals); - } else if (oracleType === "aerodrome") { - const actual = await aerodromeOracle!.tokenPools(market.token); - expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); - } else { - const actual = await uniswapOracle.tokenPools(market.token); - expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(market.pool)); - } - }); - - it(`${market.symbol} oracle is configured on SentinelOracle`, async () => { - const tc = await sentinelOracle.tokenConfigs(market.token); - const actual = tc.oracle ?? tc; - expect(ethers.utils.getAddress(actual)).to.equal(ethers.utils.getAddress(expectedDexOracle)); - }); - - it(`${market.symbol} deviation threshold is configured on DeviationSentinel`, async () => { - const tc = await deviationSentinel.tokenConfigs(market.token); - expect(tc.deviation).to.equal(market.deviationPercent); - expect(tc.enabled).to.equal(true); - }); - - // End-to-end price-pipeline check: SentinelOracle → UniswapOracle → pool.token0/token1. - // If the pool isn't a real Uniswap V3 / V3-compatible contract, token0() reverts and - // handleDeviation would always revert for this market (silent monitoring outage). - it(`${market.symbol} SentinelOracle.getPrice returns a non-zero price`, async () => { - const price = await sentinelOracle.getPrice(market.token); - expect(price, `${market.symbol}: SentinelOracle.getPrice returned 0`).to.be.gt(0); - }); - - // Full handleDeviation pre-flight: same path the keeper exercises, minus the - // EBrake side-effect. Asserts oracle/sentinel agree at fork time and the - // computed deviation is below the configured trigger threshold. - // Skipped for fork-fragile tokens (see SKIP_CHECK_PRICE_DEVIATION at top). - if (!SKIP_CHECK_PRICE_DEVIATION.has(market.symbol)) { - it(`${market.symbol} DeviationSentinel.checkPriceDeviation returns hasDeviation=false at fork time`, async () => { - const vToken = vTokenByUnderlying.get(market.token.toLowerCase()); - expect(vToken, `${market.symbol}: no vToken in Core Pool with underlying=${market.token}`).to.not.be - .undefined; - const [hasDeviation, oraclePrice, sentinelPrice, deviationPercent] = - await deviationSentinel.checkPriceDeviation(vToken); - expect(oraclePrice, `${market.symbol}: oraclePrice = 0`).to.be.gt(0); - expect(sentinelPrice, `${market.symbol}: sentinelPrice = 0`).to.be.gt(0); - expect(deviationPercent, `${market.symbol}: live deviation ≥ trigger threshold`).to.be.lt( - market.deviationPercent, - ); - expect(hasDeviation, `${market.symbol}: handleDeviation would trigger right now`).to.equal(false); - }); - } - } - }); -}; diff --git a/vips/vip-616/addresses/arbitrumone.ts b/vips/vip-616/addresses/arbitrumone.ts index 39e3b5aa9..bbca22e1a 100644 --- a/vips/vip-616/addresses/arbitrumone.ts +++ b/vips/vip-616/addresses/arbitrumone.ts @@ -1,6 +1,8 @@ -import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; import { LzChainId } from "src/types"; +import type { ChainConfig } from "../bscmainnet"; + const { arbitrumone } = NETWORK_ADDRESSES; // Already-deployed governance + protocol addresses on Arbitrum One @@ -72,4 +74,5 @@ export const ARBITRUMONE_CONFIG = { multisigPauser: ARBITRUMONE_MULTISIG_PAUSER, keeper: ARBITRUMONE_KEEPER, monitoredMarkets: ARBITRUMONE_MONITORED_MARKETS, -} as const; + configurator: ZERO_ADDRESS, // TODO: deploy DeviationSentinelConfiguratorArbitrumOne and update +} satisfies ChainConfig; diff --git a/vips/vip-616/addresses/basemainnet.ts b/vips/vip-616/addresses/basemainnet.ts index 170a2032b..bb3ea236e 100644 --- a/vips/vip-616/addresses/basemainnet.ts +++ b/vips/vip-616/addresses/basemainnet.ts @@ -1,6 +1,8 @@ -import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; import { LzChainId } from "src/types"; +import type { ChainConfig } from "../bscmainnet"; + const { basemainnet } = NETWORK_ADDRESSES; // Already-deployed governance + protocol addresses on Base mainnet @@ -73,4 +75,5 @@ export const BASEMAINNET_CONFIG = { multisigPauser: BASEMAINNET_MULTISIG_PAUSER, keeper: BASEMAINNET_KEEPER, monitoredMarkets: BASEMAINNET_MONITORED_MARKETS, -} as const; + configurator: ZERO_ADDRESS, // TODO: deploy DeviationSentinelConfiguratorBaseMainnet and update +} satisfies ChainConfig; diff --git a/vips/vip-616/addresses/ethereum.ts b/vips/vip-616/addresses/ethereum.ts index 825041408..d2159bbda 100644 --- a/vips/vip-616/addresses/ethereum.ts +++ b/vips/vip-616/addresses/ethereum.ts @@ -1,6 +1,8 @@ -import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; import { LzChainId } from "src/types"; +import type { ChainConfig } from "../bscmainnet"; + const { ethereum } = NETWORK_ADDRESSES; // Already-deployed governance + protocol addresses on Ethereum mainnet @@ -114,8 +116,8 @@ export const ETHEREUM_CONFIG = { sentinelOracle: ETHEREUM_SENTINEL_ORACLE, uniswapOracle: ETHEREUM_UNISWAP_ORACLE, curveOracle: ETHEREUM_CURVE_ORACLE, - multisigPauser: ETHEREUM_MULTISIG_PAUSER, keeper: ETHEREUM_KEEPER, monitoredMarkets: ETHEREUM_MONITORED_MARKETS, -} as const; + configurator: ZERO_ADDRESS, // TODO: deploy DeviationSentinelConfiguratorEthereum and update +} satisfies ChainConfig; diff --git a/vips/vip-616/bscmainnet.ts b/vips/vip-616/bscmainnet.ts index 6cfacda75..dd0ec4094 100644 --- a/vips/vip-616/bscmainnet.ts +++ b/vips/vip-616/bscmainnet.ts @@ -1,10 +1,16 @@ -import { Command, ProposalType } from "src/types"; +import { ZERO_ADDRESS } from "src/networkAddresses"; +import { Command, LzChainId, ProposalType } from "src/types"; import { makeProposal } from "src/utils"; import { ARBITRUMONE_CONFIG } from "./addresses/arbitrumone"; import { BASEMAINNET_CONFIG } from "./addresses/basemainnet"; import { ETHEREUM_CONFIG } from "./addresses/ethereum"; +// --------------------------------------------------------------------------- +// Shared types + ACM permission constants. Exported for simulation use; the +// VIP itself only delegates into per-chain DeviationSentinelConfigurator.execute(). +// --------------------------------------------------------------------------- + export type OracleType = "uniswap" | "curve" | "aerodrome"; export interface MonitoredMarket { @@ -28,7 +34,7 @@ export interface MonitoredMarket { export interface ChainConfig { name: string; - dstChainId: number; + dstChainId: LzChainId; acm: string; guardian: string; normalTimelock: string; @@ -39,13 +45,13 @@ export interface ChainConfig { eBrake: string; sentinelOracle: string; uniswapOracle: string; - // Optional, per-chain DEX oracles. Present only on chains that need them; bootstrap - // permissions + ownership transfer are conditionally added when set. curveOracle?: string; aerodromeOracle?: string; multisigPauser: string; keeper: string; monitoredMarkets: MonitoredMarket[]; + // Per-chain DeviationSentinelConfigurator address. ZERO_ADDRESS until deployed. + configurator: string; } export const governanceAccounts = (cfg: ChainConfig): string[] => [ @@ -55,8 +61,6 @@ export const governanceAccounts = (cfg: ChainConfig): string[] => [ cfg.criticalTimelock, ]; -const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, BASEMAINNET_CONFIG]; - // setTokenConfig uses the struct-tuple form (uint8,bool), matching DeviationSentinel.sol. export const SENTINEL_ADMIN_PERMS = [ "setTrustedKeeper(address,bool)", @@ -120,33 +124,24 @@ export const DIAMOND_ONLY_EBRAKE_PERMS = [ "decreaseCF(address,uint96,uint256)", ]; -export const grant = (acm: string, contract: string, sig: string, account: string, dstChainId: number) => ({ - target: acm, - signature: "giveCallPermission(address,string,address)", - params: [contract, sig, account], - dstChainId, -}); - -// VIP-616 (Sub-A): bootstrap + permissions. Kept under each chain's block gas -// limit by deferring governance EBrake action grants and market wiring to VIP-617. -// Per-chain command count varies with the optional CurveOracle (Ethereum) and -// AerodromeSlipstreamOracle (Base): each adds 1 acceptOwnership + 4 admin grants. -// - Ethereum: 60 + 5 (CurveOracle) = 65 -// - Arbitrum: 60 = 60 -// - Base: 60 + 5 (AerodromeSlipstream) = 65 -const buildChainCommandsA = (cfg: ChainConfig): Command[] => { - const { acm, dstChainId } = cfg; - const govAccounts = governanceAccounts(cfg); - const trustedKeeperAccounts = [cfg.keeper, ...govAccounts]; +// Per-chain configs are the single source of truth — see addresses/.ts. +// Adding a new chain only requires creating an addresses/.ts and +// extending this array. +const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, BASEMAINNET_CONFIG]; + +const buildChainCommands = (cfg: ChainConfig): Command[] => { + if (cfg.configurator === ZERO_ADDRESS) { + throw new Error(`${cfg.name}: configurator address unset; deploy and update addresses/.ts.`); + } + + const { acm, dstChainId, configurator } = cfg; const ownershipTargets: string[] = [cfg.deviationSentinel, cfg.sentinelOracle, cfg.uniswapOracle, cfg.eBrake]; if (cfg.curveOracle) ownershipTargets.push(cfg.curveOracle); if (cfg.aerodromeOracle) ownershipTargets.push(cfg.aerodromeOracle); return [ - // 1. Accept ownership of the newly deployed contracts. The deployer transfers - // ownership to the local Normal Timelock prior to this VIP. Always 4 (the - // base stack); +1 each for CurveOracle / AerodromeSlipstreamOracle when present. + // Accept pending-ownership ...ownershipTargets.map(target => ({ target, signature: "acceptOwnership()", @@ -154,80 +149,55 @@ const buildChainCommandsA = (cfg: ChainConfig): Command[] => { dstChainId, })), - // 2. Grant Guardian + governance Timelocks admin permissions on DeviationSentinel - ...govAccounts.flatMap(account => - SENTINEL_ADMIN_PERMS.map(sig => grant(acm, cfg.deviationSentinel, sig, account, dstChainId)), - ), - - // 3. Grant Guardian + governance Timelocks admin permissions on SentinelOracle - ...govAccounts.flatMap(account => - SENTINEL_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.sentinelOracle, sig, account, dstChainId)), - ), - - // 4. Grant Guardian + governance Timelocks admin permission on UniswapOracle - ...govAccounts.flatMap(account => - UNISWAP_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.uniswapOracle, sig, account, dstChainId)), - ), - - // 4b. Grant Guardian + governance Timelocks admin permission on CurveOracle (when present) - ...(cfg.curveOracle - ? govAccounts.flatMap(account => - CURVE_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.curveOracle as string, sig, account, dstChainId)), - ) - : []), - - // 4c. Grant Guardian + governance Timelocks admin permission on AerodromeSlipstreamOracle (when present) - ...(cfg.aerodromeOracle - ? govAccounts.flatMap(account => - AERODROME_ORACLE_ADMIN_PERMS.map(sig => grant(acm, cfg.aerodromeOracle as string, sig, account, dstChainId)), - ) - : []), - - // 5. Grant EBrake the IL-Comptroller-supported emergency-action permissions - ...EBRAKE_COMPTROLLER_PERMS_IL.map(sig => grant(acm, cfg.comptroller, sig, cfg.eBrake, dstChainId)), - - // 6. Grant Guardian + governance Timelocks granular snapshot-reset perms on EBrake - ...govAccounts.flatMap(account => RESET_PERMS.map(sig => grant(acm, cfg.eBrake, sig, account, dstChainId))), - - // 7. Grant DeviationSentinel the three EBrake actions handleDeviation invokes - ...SENTINEL_EBRAKE_PERMS.map(sig => grant(acm, cfg.eBrake, sig, cfg.deviationSentinel, dstChainId)), - - // 8. Grant the per-chain 1-of-1 Multisig Pauser the IL-supported EBrake action - // functions, so the Venus team can manually trigger emergency actions during - // the early operational phase (mirror of VIP-610 step 7). - ...GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, cfg.multisigPauser, dstChainId)), - - // 9. Whitelist Keeper + Guardian + governance Timelocks as trusted keepers on - // DeviationSentinel so VIPs (and the off-chain keeper) can invoke handleDeviation. - ...trustedKeeperAccounts.map(account => ({ - target: cfg.deviationSentinel, - signature: "setTrustedKeeper(address,bool)", - params: [account, true], + // Grant DEFAULT_ADMIN_ROLE on the local ACM so execute() can call giveCallPermission / + // revokeCallPermission (both internally call grantRole / revokeRole, which require + // DEFAULT_ADMIN_ROLE per OZ AccessControl). The configurator renounces this role at the + // very end of execute() via _selfRevokeACMPermissions(). + { + target: acm, + signature: "grantRole(bytes32,address)", + params: ["0x0000000000000000000000000000000000000000000000000000000000000000", configurator], dstChainId, - })), + }, + + // Apply all grants + market wiring atomically. See DeviationSentinelConfigurator.execute(). + { + target: configurator, + signature: "execute()", + params: [], + dstChainId, + }, ]; }; export const vip616 = () => { const meta = { version: "v2", - title: - "VIP-616 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2 — Bootstrap & Permissions (1/2)", - description: `#### Context + title: "VIP-616 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2", + description: `#### Description + +This VIP configures the **DeviationSentinel** + **EBrakeV2** Emergency Brake stack on **Ethereum**, **Arbitrum One**, and **Base**, mirroring the BSC setup from VIP-590 + VIP-610. Each chain's DeviationSentinel routes automated oracle-deviation enforcement through a local EBrakeV2, which applies per-action, per-market restrictions (pause borrow/supply, zero collateral factor) without manual intervention. + +The full bootstrap — ownership transfer, ACM grants for Guardian + 3 Timelocks, EBrake action permissions, multisig-pauser permissions, trusted-keeper whitelisting, and per-market deviation wiring across UniswapOracle / CurveOracle / AerodromeSlipstreamOracle — is encoded inside a per-chain **DeviationSentinelConfigurator** contract deployed to venus-periphery. + +The on-chain VIP needs only to (1) accept ownership of the periphery contracts, (2) grant the configurator \`DEFAULT_ADMIN_ROLE\` on the local ACM (required by OZ \`grantRole\` / \`revokeRole\`, which both \`giveCallPermission\` and \`revokeCallPermission\` wrap internally), and (3) call \`configurator.execute()\`. The configurator applies every grant and wiring atomically, then renounces \`DEFAULT_ADMIN_ROLE\` at the very end, so it retires permanently in a single transaction with no follow-up cleanup VIP required. + +This packaging keeps the cross-chain payload tiny — well under LayerZero V1's RelayerV2 payload-size ceiling — without splitting the bootstrap across multiple VIPs. + +In addition to UniswapOracle, two extra DEX oracles are bootstrapped on the chains that need them: + +- **CurveOracle (Ethereum only)** — prices eBTC against WBTC via \`get_dy\` on the eBTC/WBTC Curve StableSwap-NG pool, which the existing UniswapOracle can't read (StableSwap-NG isn't Uniswap V3 ABI-compatible). +- **AerodromeSlipstreamOracle (Base only)** — prices cbBTC and wstETH on the most liquid Aerodrome Slipstream pools (cbBTC/USDC and wstETH/WETH). Aerodrome Slipstream's \`slot0()\` returns a 6-tuple (no \`feeProtocol\`) so the Solidity decoder reverts when read against the Uniswap V3 7-tuple ABI used by UniswapOracle. -Deploys the DeviationSentinel + EBrakeV2 stack on the three non-BSC chains — the same oracle-manipulation protection layer that's been running on BSC since VIP-590 / VIP-610. This VIP accepts ownership of the newly deployed contracts and wires up the permissions between Sentinel, EBrake, the IL Comptroller, governance, and the Multisig Pauser. The actual market monitoring is turned on in VIP-617. +Because EBrake on these chains uses \`isIsolatedPool=true\` (single-pool IL Comptroller, not the BSC Diamond), only the IL-supported subset of EBrake action functions is granted. Diamond-only functions (\`pauseFlashLoan\`, \`disablePoolBorrow\`, \`revokeFlashLoanAccess\`, \`decreaseCF(address,uint96,uint256)\`) are omitted as they revert on IL comptrollers. -#### Per-chain Actions (×3 chains) +#### Summary -For each chain, the VIP performs the following 7 steps: +If approved, this VIP will, for each of Ethereum, Arbitrum One, and Base: -1. **Accept ownership** of the newly deployed contracts: DeviationSentinel, SentinelOracle, UniswapOracle, EBrake (+ CurveOracle on Ethereum, + AerodromeSlipstreamOracle on Base). -2. **Grant admin perms** to Guardian + 3 Timelocks (Normal / Fast-track / Critical) on each oracle and the DeviationSentinel — so governance can update pool configs and monitoring settings. -3. **Authorize EBrake → IL Comptroller** for the 4 emergency action types (pause, collateral factor, borrow cap, supply cap). -4. **Grant snapshot-reset perms on EBrake** to Guardian + 3 Timelocks — so governance can restore market config after a brake event fires. -5. **Authorize DeviationSentinel → EBrake** for the 3 actions Sentinel auto-invokes when a deviation triggers (pause borrow, pause supply, decrease CF). -6. **Grant the Multisig Pauser the 8 IL-supported EBrake actions** — manual emergency control during the early operational phase. -7. **Whitelist trusted keepers** on DeviationSentinel — the off-chain keeper + Guardian + 3 Timelocks. +- Accept governance ownership of **DeviationSentinel**, **SentinelOracle**, **UniswapOracle**, and **EBrakeV2** (plus **CurveOracle** on Ethereum and **AerodromeSlipstreamOracle** on Base) +- Grant the per-chain **DeviationSentinelConfigurator** \`DEFAULT_ADMIN_ROLE\` on the local ACM so \`execute()\` can call \`giveCallPermission\` / \`revokeCallPermission\` (both wrap OZ \`grantRole\` / \`revokeRole\`, which require that role). The configurator renounces \`DEFAULT_ADMIN_ROLE\` at the end of \`execute()\` so it retires permanently. +- Invoke \`configurator.execute()\`, which atomically applies all admin grants, EBrake permissions, multisig-pauser permissions, trusted-keeper whitelisting, and per-market deviation wiring at the unified 10% threshold (10 markets on Ethereum, 5 on Arbitrum One, 4 on Base) — and renounces its own \`DEFAULT_ADMIN_ROLE\` at the end #### References @@ -240,7 +210,7 @@ For each chain, the VIP performs the following 7 steps: abstainDescription: "Indifferent to execution", }; - return makeProposal(NETWORKS.flatMap(buildChainCommandsA), meta, ProposalType.REGULAR); + return makeProposal(NETWORKS.flatMap(buildChainCommands), meta, ProposalType.REGULAR); }; export default vip616; diff --git a/vips/vip-616/bsctestnet.ts b/vips/vip-616/bsctestnet.ts index d715d1413..79e5e66f2 100644 --- a/vips/vip-616/bsctestnet.ts +++ b/vips/vip-616/bsctestnet.ts @@ -1,8 +1,18 @@ -import { ProposalType } from "src/types"; +import { Command, LzChainId, ProposalType } from "src/types"; import { makeProposal } from "src/utils"; import { SEPOLIA_CONFIG, SEPOLIA_GUARDIAN_OWNER } from "./addresses/sepolia"; -import { EBRAKE_COMPTROLLER_PERMS_IL, SENTINEL_EBRAKE_PERMS, grant } from "./bscmainnet"; +import { EBRAKE_COMPTROLLER_PERMS_IL, SENTINEL_EBRAKE_PERMS } from "./bscmainnet"; + +// ACM `giveCallPermission` command builder. Inlined here because this VIP is +// the only consumer — the mainnet VIP delegates all grants into the +// configurator helper and doesn't need it. +const grant = (acm: string, target: string, signature: string, account: string, dstChainId: LzChainId): Command => ({ + target: acm, + signature: "giveCallPermission(address,string,address)", + params: [target, signature, account], + dstChainId, +}); // Exported for simulation imports. export const DEPLOYER_SENTINEL_ORACLE_PERMS = ["setDirectPrice(address,uint256)"]; diff --git a/vips/vip-617/bscmainnet.ts b/vips/vip-617/bscmainnet.ts deleted file mode 100644 index f914861b1..000000000 --- a/vips/vip-617/bscmainnet.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { ZERO_ADDRESS } from "src/networkAddresses"; -import { Command, ProposalType } from "src/types"; -import { makeProposal } from "src/utils"; - -import { ARBITRUMONE_CONFIG } from "../vip-616/addresses/arbitrumone"; -import { BASEMAINNET_CONFIG } from "../vip-616/addresses/basemainnet"; -import { ETHEREUM_CONFIG } from "../vip-616/addresses/ethereum"; -import { - ChainConfig, - GOVERNANCE_EBRAKE_PERMS_IL, - MonitoredMarket, - governanceAccounts, - grant, -} from "../vip-616/bscmainnet"; - -const NETWORKS: ChainConfig[] = [ETHEREUM_CONFIG, ARBITRUMONE_CONFIG, BASEMAINNET_CONFIG]; - -// Resolve the DEX oracle for a market based on its `oracleType`. Defaults to UniswapOracle. -// Misconfiguration (e.g. oracleType="curve" on a chain without a CurveOracle deployment) -// surfaces here at proposal-build time rather than at on-chain execution. -const resolveDexOracle = (cfg: ChainConfig, market: MonitoredMarket): string => { - switch (market.oracleType ?? "uniswap") { - case "uniswap": - return cfg.uniswapOracle; - case "curve": - if (!cfg.curveOracle) throw new Error(`${cfg.name}: ${market.symbol} requires curveOracle in ChainConfig`); - return cfg.curveOracle; - case "aerodrome": - if (!cfg.aerodromeOracle) - throw new Error(`${cfg.name}: ${market.symbol} requires aerodromeOracle in ChainConfig`); - return cfg.aerodromeOracle; - } -}; - -// Build the DEX-side `setPoolConfig` call for a market. UniswapOracle and AerodromeSlipstreamOracle -// share the (token, pool) signature; CurveOracle takes additional (coinIndex, refCoinIndex, -// referenceToken, assetDecimals) fields for its get_dy()-based pricing scheme. -const buildSetPoolCommand = (cfg: ChainConfig, market: MonitoredMarket, dstChainId: number): Command => { - const oracle = resolveDexOracle(cfg, market); - if ((market.oracleType ?? "uniswap") === "curve") { - if ( - market.coinIndex === undefined || - market.refCoinIndex === undefined || - !market.referenceToken || - market.assetDecimals === undefined - ) { - throw new Error( - `${cfg.name}: ${market.symbol} (curve) requires coinIndex + refCoinIndex + referenceToken + assetDecimals`, - ); - } - return { - target: oracle, - signature: "setPoolConfig(address,address,uint8,uint8,address,uint8)", - params: [ - market.token, - market.pool, - market.coinIndex, - market.refCoinIndex, - market.referenceToken, - market.assetDecimals, - ], - dstChainId, - }; - } - return { - target: oracle, - signature: "setPoolConfig(address,address)", - params: [market.token, market.pool], - dstChainId, - }; -}; - -// VIP-617 (Sub-B): governance EBrake action grants + per-market wiring. -// Per-chain command count: gov ebrake action 32 + 3 × eligible markets. -// - Ethereum: 32 + 30 (10 mkts: 9 Uniswap + 1 Curve) = 62 -// - Arbitrum: 32 + 15 ( 5 mkts: 5 Uniswap) = 47 -// - Base: 32 + 12 ( 4 mkts: 2 Uniswap + 2 Aero) = 44 -// All under their respective block gas limits with the LayerZero adapter param. -const buildChainCommandsB = (cfg: ChainConfig): Command[] => { - const { acm, dstChainId } = cfg; - - const commands: Command[] = [ - // 1. Grant Guardian + governance Timelocks the IL-supported EBrake action functions - ...governanceAccounts(cfg).flatMap(account => - GOVERNANCE_EBRAKE_PERMS_IL.map(sig => grant(acm, cfg.eBrake, sig, account, dstChainId)), - ), - ]; - - // 2. Per-market wiring. For each eligible market, configure the DEX pool on the - // chain-appropriate DEX oracle (Uniswap / Curve / Aerodrome Slipstream), point the - // SentinelOracle at that oracle for the token, then enable deviation monitoring on - // the DeviationSentinel. Markets with a ZERO_ADDRESS token or pool are skipped - // (placeholder safety). - for (const market of cfg.monitoredMarkets) { - if (market.token === ZERO_ADDRESS || market.pool === ZERO_ADDRESS) continue; - const dexOracle = resolveDexOracle(cfg, market); - commands.push( - buildSetPoolCommand(cfg, market, dstChainId), - { - target: cfg.sentinelOracle, - signature: "setTokenOracleConfig(address,address)", - params: [market.token, dexOracle], - dstChainId, - }, - { - target: cfg.deviationSentinel, - signature: "setTokenConfig(address,(uint8,bool))", - params: [market.token, [market.deviationPercent, true]], - dstChainId, - }, - ); - } - - return commands; -}; - -export const vip617 = () => { - const meta = { - version: "v2", - title: - "VIP-617 [Ethereum, Arbitrum One, Base] Configure DeviationSentinel + EBrakeV2 — Governance Actions & Market Wiring (2/2)", - description: `#### Context - -With ownership and permissions in place from VIP-616, this VIP grants governance the ability to manually invoke EBrake actions and turns on deviation monitoring for the 19 eligible markets at a unified 10% threshold. - -#### Per-chain Actions - -For each chain, the VIP performs the following 2 blocks of work: - -1. **Grant the 8 IL-supported EBrake action perms** to Guardian + 3 Timelocks (same set Multisig Pauser receives in VIP-616 step 6). -2. **For each monitored market** — 3 calls per market: - - Bind the underlying token to its DEX pool on the appropriate DEX oracle - - Route SentinelOracle to that DEX oracle for the token - - Enable DeviationSentinel monitoring at the 10% threshold - -#### DEX oracles routed - -- **Uniswap V3** (UniswapOracle) — default, covers most markets on all 3 chains -- **Curve** (CurveOracle) — Ethereum only, for the eBTC/WBTC StableSwap-NG pool -- **Aerodrome Slipstream** (AerodromeSlipstreamOracle) — Base only, for cbBTC + wstETH - -#### References - -- [VIP-616 (Bootstrap & Permissions)](https://app.venus.io/governance/proposal/616) -- [VIP-590 (BSC)](https://app.venus.io/governance/proposal/590) -- [VIP-610 (BSC)](https://app.venus.io/governance/proposal/610) -- [Original Proposal: Emergency Brake — Price Deviation Safeguard Mechanism](https://community.venus.io/t/proposal-emergency-brake-price-deviation-safeguard-mechanism/5668) -- [GitHub PR](https://github.com/VenusProtocol/vips/pull/702)`, - forDescription: "Execute this proposal", - againstDescription: "Do not execute this proposal", - abstainDescription: "Indifferent to execution", - }; - - return makeProposal(NETWORKS.flatMap(buildChainCommandsB), meta, ProposalType.REGULAR); -}; - -export default vip617; From 97a61ffd367f9bc3c26e2c313eb04c15c345285a Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 5 May 2026 13:45:40 +0530 Subject: [PATCH 15/15] feat(vip-616): update fork blocks and configurator addresses --- simulations/vip-616/arbitrumone.ts | 2 +- simulations/vip-616/basemainnet.ts | 2 +- simulations/vip-616/ethereum.ts | 2 +- vips/vip-616/addresses/arbitrumone.ts | 6 +++--- vips/vip-616/addresses/basemainnet.ts | 5 +++-- vips/vip-616/addresses/ethereum.ts | 5 +++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/simulations/vip-616/arbitrumone.ts b/simulations/vip-616/arbitrumone.ts index c95126e42..bd502f15d 100644 --- a/simulations/vip-616/arbitrumone.ts +++ b/simulations/vip-616/arbitrumone.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { ARBITRUMONE_CONFIG } from "../../vips/vip-616/addresses/arbitrumone"; import { runVip616Suite } from "./shared"; -const FORK_BLOCK = 457580585; // TODO: update to block after DeviationSentinelConfiguratorArbitrumOne deployment +const FORK_BLOCK = 459572065; forking(FORK_BLOCK, async () => { await runVip616Suite(ARBITRUMONE_CONFIG); diff --git a/simulations/vip-616/basemainnet.ts b/simulations/vip-616/basemainnet.ts index 89b917423..dd8e28ec9 100644 --- a/simulations/vip-616/basemainnet.ts +++ b/simulations/vip-616/basemainnet.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { BASEMAINNET_CONFIG } from "../../vips/vip-616/addresses/basemainnet"; import { runVip616Suite } from "./shared"; -const FORK_BLOCK = 45338981; // TODO: update to block after DeviationSentinelConfiguratorBaseMainnet deployment +const FORK_BLOCK = 45589260; forking(FORK_BLOCK, async () => { await runVip616Suite(BASEMAINNET_CONFIG); diff --git a/simulations/vip-616/ethereum.ts b/simulations/vip-616/ethereum.ts index d0af3e1e1..735e82b27 100644 --- a/simulations/vip-616/ethereum.ts +++ b/simulations/vip-616/ethereum.ts @@ -3,7 +3,7 @@ import { forking } from "src/vip-framework"; import { ETHEREUM_CONFIG } from "../../vips/vip-616/addresses/ethereum"; import { runVip616Suite } from "./shared"; -const FORK_BLOCK = 24992853; // TODO: update to block after DeviationSentinelConfiguratorEthereum deployment +const FORK_BLOCK = 25027435; forking(FORK_BLOCK, async () => { await runVip616Suite(ETHEREUM_CONFIG); diff --git a/vips/vip-616/addresses/arbitrumone.ts b/vips/vip-616/addresses/arbitrumone.ts index bbca22e1a..a24e022b5 100644 --- a/vips/vip-616/addresses/arbitrumone.ts +++ b/vips/vip-616/addresses/arbitrumone.ts @@ -1,4 +1,4 @@ -import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { LzChainId } from "src/types"; import type { ChainConfig } from "../bscmainnet"; @@ -20,7 +20,7 @@ export const ARBITRUMONE_SENTINEL_ORACLE = "0x3563CAbc541a0432C66A64942ffB4070a9 export const ARBITRUMONE_UNISWAP_ORACLE = "0xB6CFbfe6834EF519f002DBc1a8B81Ea437Ca647D"; export const ARBITRUMONE_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig export const ARBITRUMONE_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; - +export const ARBITRUMONE_CONFIGURATOR = "0x2AFAed4A909491E0181E55429F7F621528BEd5Ef"; export const ARBITRUMONE_DST_CHAIN_ID = LzChainId.arbitrumone; // Eligible Core Pool markets — Uniswap V3 (Arbitrum) sources, unified 10% threshold. @@ -74,5 +74,5 @@ export const ARBITRUMONE_CONFIG = { multisigPauser: ARBITRUMONE_MULTISIG_PAUSER, keeper: ARBITRUMONE_KEEPER, monitoredMarkets: ARBITRUMONE_MONITORED_MARKETS, - configurator: ZERO_ADDRESS, // TODO: deploy DeviationSentinelConfiguratorArbitrumOne and update + configurator: ARBITRUMONE_CONFIGURATOR, } satisfies ChainConfig; diff --git a/vips/vip-616/addresses/basemainnet.ts b/vips/vip-616/addresses/basemainnet.ts index bb3ea236e..64678f5d3 100644 --- a/vips/vip-616/addresses/basemainnet.ts +++ b/vips/vip-616/addresses/basemainnet.ts @@ -1,4 +1,4 @@ -import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { LzChainId } from "src/types"; import type { ChainConfig } from "../bscmainnet"; @@ -23,6 +23,7 @@ export const BASEMAINNET_UNISWAP_ORACLE = "0xc3b5169a7d5f6341403c74187Db3C4Fe6d4 export const BASEMAINNET_AERODROME_ORACLE = "0x5DE0B322A74088fD64CDD01042BE2fBc47FE82EC"; export const BASEMAINNET_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig export const BASEMAINNET_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; +export const BASEMAINNET_CONFIGURATOR = "0x3fb0aD3828E6A62E3B527CF51F4F3d011B4a9EA1"; export const BASEMAINNET_DST_CHAIN_ID = LzChainId.basemainnet; @@ -75,5 +76,5 @@ export const BASEMAINNET_CONFIG = { multisigPauser: BASEMAINNET_MULTISIG_PAUSER, keeper: BASEMAINNET_KEEPER, monitoredMarkets: BASEMAINNET_MONITORED_MARKETS, - configurator: ZERO_ADDRESS, // TODO: deploy DeviationSentinelConfiguratorBaseMainnet and update + configurator: BASEMAINNET_CONFIGURATOR, } satisfies ChainConfig; diff --git a/vips/vip-616/addresses/ethereum.ts b/vips/vip-616/addresses/ethereum.ts index d2159bbda..224b481bd 100644 --- a/vips/vip-616/addresses/ethereum.ts +++ b/vips/vip-616/addresses/ethereum.ts @@ -1,4 +1,4 @@ -import { NETWORK_ADDRESSES, ZERO_ADDRESS } from "src/networkAddresses"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { LzChainId } from "src/types"; import type { ChainConfig } from "../bscmainnet"; @@ -23,6 +23,7 @@ export const ETHEREUM_UNISWAP_ORACLE = "0x873993F8f5f5Ddbae0952e939ab3005Af363Af export const ETHEREUM_CURVE_ORACLE = "0x9F508F3146cb03276282f9237c6eE64f76E3261D"; export const ETHEREUM_MULTISIG_PAUSER = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948"; // Venus team multisig export const ETHEREUM_KEEPER = "0x57fa23f591203f61cef84a7bc892df69ca95c86e"; +export const ETHEREUM_CONFIGURATOR = "0x8E84b25144de0eA1d9D6E126b85769B60f4D604b"; // Reference token for eBTC's CurveOracle entry — see CurveOracle.sol for the pricing math. const WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"; @@ -119,5 +120,5 @@ export const ETHEREUM_CONFIG = { multisigPauser: ETHEREUM_MULTISIG_PAUSER, keeper: ETHEREUM_KEEPER, monitoredMarkets: ETHEREUM_MONITORED_MARKETS, - configurator: ZERO_ADDRESS, // TODO: deploy DeviationSentinelConfiguratorEthereum and update + configurator: ETHEREUM_CONFIGURATOR, } satisfies ChainConfig;