diff --git a/simulations/vip-650/abi/boundValidator.json b/simulations/vip-650/abi/boundValidator.json new file mode 100644 index 000000000..bd39b8e51 --- /dev/null +++ b/simulations/vip-650/abi/boundValidator.json @@ -0,0 +1,157 @@ +[ + { "inputs": [], "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": "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": "asset", "type": "address" }, + { "indexed": true, "internalType": "uint256", "name": "upperBound", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "lowerBound", "type": "uint256" } + ], + "name": "ValidateConfigAdded", + "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": "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": [ + { + "components": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "upperBoundRatio", "type": "uint256" }, + { "internalType": "uint256", "name": "lowerBoundRatio", "type": "uint256" } + ], + "internalType": "struct BoundValidator.ValidateConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "setValidateConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "upperBoundRatio", "type": "uint256" }, + { "internalType": "uint256", "name": "lowerBoundRatio", "type": "uint256" } + ], + "internalType": "struct BoundValidator.ValidateConfig[]", + "name": "configs", + "type": "tuple[]" + } + ], + "name": "setValidateConfigs", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "validateConfigs", + "outputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "upperBoundRatio", "type": "uint256" }, + { "internalType": "uint256", "name": "lowerBoundRatio", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "reportedPrice", "type": "uint256" }, + { "internalType": "uint256", "name": "anchorPrice", "type": "uint256" } + ], + "name": "validatePriceWithAnchorPrice", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-650/abi/chainlinkOracle.json b/simulations/vip-650/abi/chainlinkOracle.json new file mode 100644 index 000000000..7dd5f6f5f --- /dev/null +++ b/simulations/vip-650/abi/chainlinkOracle.json @@ -0,0 +1,187 @@ +[ + { "inputs": [], "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": "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": "asset", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "previousPriceMantissa", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "newPriceMantissa", "type": "uint256" } + ], + "name": "PricePosted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "asset", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "feed", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "maxStalePeriod", "type": "uint256" } + ], + "name": "TokenConfigAdded", + "type": "event" + }, + { + "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": [{ "internalType": "address", "name": "asset", "type": "address" }], + "name": "getPrice", + "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": "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": "prices", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "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": "uint256", "name": "price", "type": "uint256" } + ], + "name": "setDirectPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "address", "name": "feed", "type": "address" }, + { "internalType": "uint256", "name": "maxStalePeriod", "type": "uint256" } + ], + "internalType": "struct ChainlinkOracle.TokenConfig", + "name": "tokenConfig", + "type": "tuple" + } + ], + "name": "setTokenConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "address", "name": "feed", "type": "address" }, + { "internalType": "uint256", "name": "maxStalePeriod", "type": "uint256" } + ], + "internalType": "struct ChainlinkOracle.TokenConfig[]", + "name": "tokenConfigs_", + "type": "tuple[]" + } + ], + "name": "setTokenConfigs", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "tokenConfigs", + "outputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "address", "name": "feed", "type": "address" }, + { "internalType": "uint256", "name": "maxStalePeriod", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-650/abi/resilientOracle.json b/simulations/vip-650/abi/resilientOracle.json new file mode 100644 index 000000000..373eb14e9 --- /dev/null +++ b/simulations/vip-650/abi/resilientOracle.json @@ -0,0 +1,320 @@ +[ + { + "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": true, "internalType": "address", "name": "asset", "type": "address" }, + { "indexed": true, "internalType": "bool", "name": "enabled", "type": "bool" } + ], + "name": "CachedEnabled", + "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": "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": "CACHE_SLOT", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "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": "bool", "name": "cachingEnabled", "type": "bool" } + ], + "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": "bool", "name": "cachingEnabled", "type": "bool" } + ], + "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": "bool", "name": "cachingEnabled", "type": "bool" } + ], + "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-650/bscmainnet.ts b/simulations/vip-650/bscmainnet.ts new file mode 100644 index 000000000..79edebaf0 --- /dev/null +++ b/simulations/vip-650/bscmainnet.ts @@ -0,0 +1,175 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { + expectEvents, + initMainnetUser, + setMaxStalePeriodInBinanceOracle, + setMaxStalePeriodInChainlinkOracle, +} from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import vip650, { + BOUND_VALIDATOR, + NEW_ORACLE_CONFIG, + NEW_REDSTONE_ORACLE_FEEDS, + NEW_STANDARD_ORACLE_CONFIG, + NEW_TWT_ORACLE_CONFIG, + OLD_ORACLE_CONFIG, + PRICE_LOWER_BOUND, + PRICE_UPPER_BOUND, +} from "../../vips/vip-650/bscmainnet"; +import BOUND_VALIDATOR_ABI from "./abi/boundValidator.json"; +import CHAINLINK_ORACLE_ABI from "./abi/chainlinkOracle.json"; +import RESILIENT_ORACLE_ABI from "./abi/resilientOracle.json"; + +const { bscmainnet } = NETWORK_ADDRESSES; +const addressZero = ethers.constants.AddressZero; + +forking(80964126, async () => { + let redstoneOracle: Contract; + let resilientOracle: Contract; + const preVipPrices: Record = {}; + + let boundValidator: Contract; + + before(async () => { + redstoneOracle = new ethers.Contract(bscmainnet.REDSTONE_ORACLE, CHAINLINK_ORACLE_ABI, ethers.provider); + resilientOracle = new ethers.Contract(bscmainnet.RESILIENT_ORACLE, RESILIENT_ORACLE_ABI, ethers.provider); + boundValidator = new ethers.Contract(BOUND_VALIDATOR, BOUND_VALIDATOR_ABI, ethers.provider); + }); + + describe("Pre-VIP behavior", async () => { + it("has no RedStone feed configured for assets to be added", async () => { + for (const { ASSET } of NEW_REDSTONE_ORACLE_FEEDS) { + const cfg = await redstoneOracle.tokenConfigs(ASSET); + expect(cfg.asset).to.equal(addressZero); + } + }); + + it("has resilient oracle configs matching old config for all assets", async () => { + for (const oldConfig of OLD_ORACLE_CONFIG) { + const cfg = await resilientOracle.getTokenConfig(oldConfig.ASSET); + + expect(cfg.asset).to.equal(oldConfig.ASSET); + expect(cfg.oracles[0]).to.equal(oldConfig.MAIN); + expect(cfg.oracles[1]).to.equal(oldConfig.PIVOT); + expect(cfg.oracles[2]).to.equal(oldConfig.FALLBACK); + expect(cfg.cachingEnabled).to.equal(oldConfig.CACHED); + } + }); + + it("has no BoundValidator config for TWT", async () => { + const cfg = await boundValidator.validateConfigs(NEW_TWT_ORACLE_CONFIG.ASSET); + expect(cfg.upperBoundRatio).to.equal(0); + expect(cfg.lowerBoundRatio).to.equal(0); + }); + + it("stores current resilient oracle prices for all assets", async () => { + for (const { ASSET } of NEW_ORACLE_CONFIG) { + const price = await resilientOracle.getPrice(ASSET); + preVipPrices[ASSET] = price; + } + }); + }); + + testVip("VIP-650 bscmainnet", await vip650(), { + callbackAfterExecution: async txResponse => { + const totalTokenConfigAdded = NEW_REDSTONE_ORACLE_FEEDS.length + NEW_ORACLE_CONFIG.length; + await expectEvents( + txResponse, + [CHAINLINK_ORACLE_ABI, RESILIENT_ORACLE_ABI], + ["TokenConfigAdded", "TokenConfigAdded"], + [totalTokenConfigAdded, totalTokenConfigAdded], + ); + await expectEvents(txResponse, [BOUND_VALIDATOR_ABI], ["ValidateConfigAdded"], [1]); + }, + }); + + describe("Post-VIP behavior", async () => { + it("sets expected RedStone oracle feeds", async () => { + for (const { ASSET, FEED, MAX_STALE_PERIOD } of NEW_REDSTONE_ORACLE_FEEDS) { + const cfg = await redstoneOracle.tokenConfigs(ASSET); + expect(cfg.asset).to.equal(ASSET); + expect(cfg.feed).to.equal(FEED); + expect(cfg.maxStalePeriod).to.equal(MAX_STALE_PERIOD); + } + }); + + it("sets expected resilient oracle config for all assets", async () => { + for (const oracleConfig of NEW_ORACLE_CONFIG) { + const cfg = await resilientOracle.getTokenConfig(oracleConfig.ASSET); + + const expectedOracles = [oracleConfig.MAIN, oracleConfig.PIVOT, oracleConfig.FALLBACK]; + + expect(cfg.asset).to.equal(oracleConfig.ASSET); + expect(cfg.oracles[0]).to.equal(expectedOracles[0]); + expect(cfg.oracles[1]).to.equal(expectedOracles[1]); + expect(cfg.oracles[2]).to.equal(expectedOracles[2]); + + expect(cfg.enableFlagsForOracles[0]).to.equal(expectedOracles[0] !== addressZero); + expect(cfg.enableFlagsForOracles[1]).to.equal(expectedOracles[1] !== addressZero); + expect(cfg.enableFlagsForOracles[2]).to.equal(expectedOracles[2] !== addressZero); + + expect(cfg.cachingEnabled).to.equal(oracleConfig.CACHED); + } + }); + + it("sets BoundValidator config for TWT", async () => { + const cfg = await boundValidator.validateConfigs(NEW_TWT_ORACLE_CONFIG.ASSET); + expect(cfg.upperBoundRatio).to.equal(PRICE_UPPER_BOUND); + expect(cfg.lowerBoundRatio).to.equal(PRICE_LOWER_BOUND); + }); + + it("keeps resilient oracle prices unchanged for all assets", async () => { + // Standard assets: extend stale periods for RedStone, Chainlink, and Binance + for (const { ASSET, NAME } of NEW_STANDARD_ORACLE_CONFIG) { + await setMaxStalePeriodInChainlinkOracle( + bscmainnet.REDSTONE_ORACLE, + ASSET, + ethers.constants.AddressZero, + bscmainnet.NORMAL_TIMELOCK, + 315360000, + ); + await setMaxStalePeriodInChainlinkOracle( + bscmainnet.CHAINLINK_ORACLE, + ASSET, + ethers.constants.AddressZero, + bscmainnet.NORMAL_TIMELOCK, + 315360000, + ); + await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, NAME, 315360000); + } + + // TWT: extend stale periods for RedStone and Binance (no Chainlink feed) + await setMaxStalePeriodInChainlinkOracle( + bscmainnet.REDSTONE_ORACLE, + NEW_TWT_ORACLE_CONFIG.ASSET, + ethers.constants.AddressZero, + bscmainnet.NORMAL_TIMELOCK, + 315360000, + ); + await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, NEW_TWT_ORACLE_CONFIG.NAME, 315360000); + + // Set direct price on RedStone oracle for TWT using the pre-VIP Binance price. + // This bypasses the feed staleness check that occurs in the fork simulation + // (the VIP execution advances time ~72h past the feed's last update). + const oracleAdmin = await initMainnetUser(bscmainnet.NORMAL_TIMELOCK, ethers.utils.parseEther("1.0")); + await redstoneOracle + .connect(oracleAdmin) + .setDirectPrice(NEW_TWT_ORACLE_CONFIG.ASSET, preVipPrices[NEW_TWT_ORACLE_CONFIG.ASSET]); + + // Verify prices haven't deviated significantly + for (const { NAME, ASSET } of NEW_ORACLE_CONFIG) { + const priceAfter = await resilientOracle.getPrice(ASSET); + const priceBefore = preVipPrices[ASSET]; + const diff = priceAfter.sub(priceBefore); + const diffPct = `${(Number(diff.mul(10000).div(priceBefore)) / 100).toFixed(2)}%`; + console.log(`${NAME}: difference = ${diff.toString()} (${diffPct})`); + const tolerance = priceBefore.mul(3).div(100); // allow 3% difference + expect(priceAfter).to.be.closeTo(priceBefore, tolerance); + } + }); + }); +}); diff --git a/vips/vip-650/bscmainnet.ts b/vips/vip-650/bscmainnet.ts new file mode 100644 index 000000000..9746dd7aa --- /dev/null +++ b/vips/vip-650/bscmainnet.ts @@ -0,0 +1,384 @@ +import { parseUnits } from "ethers/lib/utils"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const addressZero = ethers.constants.AddressZero; +const { RESILIENT_ORACLE, CHAINLINK_ORACLE, REDSTONE_ORACLE, BINANCE_ORACLE } = NETWORK_ADDRESSES.bscmainnet; + +export const BOUND_VALIDATOR = "0x6E332fF0bB52475304494E4AE5063c1051c7d735"; +export const PRICE_LOWER_BOUND = parseUnits("0.95", 18); +export const PRICE_UPPER_BOUND = parseUnits("1.05", 18); + +/* ============ New RedStone Oracle Feeds ============ */ + +export const NEW_REDSTONE_ORACLE_FEEDS = [ + { + NAME: "XVS", + ASSET: "0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63", + FEED: "0xED2B1ca5D7E246f615c2291De309643D41FeC97e", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "LTC", + ASSET: "0x4338665CBB7B2485A8855A139b75D5e34AB0DB94", + FEED: "0x7A9b672fc20b5C89D6774514052b3e0899E5E263", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "BCH", + ASSET: "0x8fF795a6F4D97E7887C79beA79aba5cc76444aDf", + FEED: "0x98ECE0D516f891a35278E3186772fb1545b274eB", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "DOT", + ASSET: "0x7083609fCE4d1d8Dc0C979AAb8c869Ea2C873402", + FEED: "0xa75CC459De167De5BC21ccdecCdB85e86377B00f", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "LINK", + ASSET: "0xF8A0BF9cF54Bb92F17374d9e9A321E6a111a51bD", + FEED: "0x1b0FDa12D125B864756Bbf191ad20eaB10915a6F", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "DAI", + ASSET: "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3", + FEED: "0x0bE6929FD4ad87347e97A525DB6ac8E884FCDCeC", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "FIL", + ASSET: "0x0D8Ce2A99Bb6e3B7Db580eD848240e4a0F9aE153", + FEED: "0xe49df9f60D6B1Dd1D5fdfCAB29216f4a8582dA86", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "DOGE", + ASSET: "0xbA2aE424d960c26247Dd6c32edC70B295c744C43", + FEED: "0x6f57Ff507735BcD3d86af83aF77ABD10395b2904", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "AAVE", + ASSET: "0xfb6115445Bff7b52FeB98650C87f44907E58f802", + FEED: "0xe4630835eA31ABD4247e449A550Fb92c8a5a4E96", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "UNI", + ASSET: "0xBf5140A22578168FD562DCcF235E5D43A02ce9B1", + FEED: "0x22d47686b3AEC9068768f84EFD8Ce2637a347B0A", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "FDUSD", + ASSET: "0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", + FEED: "0x98DC6E90D4c2f212ed9d124aD2aFBa4833268633", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "TWT", + ASSET: "0x4B0F1812e5Df2A09796481Ff14017e6005508003", + FEED: "0xefe76D1C11F267d8735D240f53317F238D8C77c9", + MAX_STALE_PERIOD: 21600, + }, + { + NAME: "SOL", + ASSET: "0x570A5D26f7765Ecb712C0924E4De545B89fD43dF", + FEED: "0x90196F6D52fce394C79D1614265d36D3F0033Ccf", + MAX_STALE_PERIOD: 21600, + }, +]; + +/* ============ Old Oracle Configs (pre-VIP) ============ */ + +export const OLD_ORACLE_CONFIG = [ + { + NAME: "XVS", + ASSET: "0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "LTC", + ASSET: "0x4338665CBB7B2485A8855A139b75D5e34AB0DB94", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "BCH", + ASSET: "0x8fF795a6F4D97E7887C79beA79aba5cc76444aDf", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "DOT", + ASSET: "0x7083609fCE4d1d8Dc0C979AAb8c869Ea2C873402", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "LINK", + ASSET: "0xF8A0BF9cF54Bb92F17374d9e9A321E6a111a51bD", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "DAI", + ASSET: "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "FIL", + ASSET: "0x0D8Ce2A99Bb6e3B7Db580eD848240e4a0F9aE153", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "DOGE", + ASSET: "0xbA2aE424d960c26247Dd6c32edC70B295c744C43", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "AAVE", + ASSET: "0xfb6115445Bff7b52FeB98650C87f44907E58f802", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "UNI", + ASSET: "0xBf5140A22578168FD562DCcF235E5D43A02ce9B1", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "FDUSD", + ASSET: "0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "TWT", + ASSET: "0x4B0F1812e5Df2A09796481Ff14017e6005508003", + MAIN: BINANCE_ORACLE, + PIVOT: addressZero, + FALLBACK: addressZero, + CACHED: false, + }, + { + NAME: "SOL", + ASSET: "0x570A5D26f7765Ecb712C0924E4De545B89fD43dF", + MAIN: CHAINLINK_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, + }, +]; + +/* ============ New Oracle Configs (post-VIP) ============ */ + +// Standard assets: RedStone MAIN, Chainlink PIVOT, Binance FALLBACK +export const NEW_STANDARD_ORACLE_CONFIG = [ + { + NAME: "XVS", + ASSET: "0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "LTC", + ASSET: "0x4338665CBB7B2485A8855A139b75D5e34AB0DB94", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "BCH", + ASSET: "0x8fF795a6F4D97E7887C79beA79aba5cc76444aDf", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "DOT", + ASSET: "0x7083609fCE4d1d8Dc0C979AAb8c869Ea2C873402", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "LINK", + ASSET: "0xF8A0BF9cF54Bb92F17374d9e9A321E6a111a51bD", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "DAI", + ASSET: "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "FIL", + ASSET: "0x0D8Ce2A99Bb6e3B7Db580eD848240e4a0F9aE153", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "DOGE", + ASSET: "0xbA2aE424d960c26247Dd6c32edC70B295c744C43", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "AAVE", + ASSET: "0xfb6115445Bff7b52FeB98650C87f44907E58f802", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "UNI", + ASSET: "0xBf5140A22578168FD562DCcF235E5D43A02ce9B1", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "FDUSD", + ASSET: "0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, + { + NAME: "SOL", + ASSET: "0x570A5D26f7765Ecb712C0924E4De545B89fD43dF", + MAIN: REDSTONE_ORACLE, + PIVOT: CHAINLINK_ORACLE, + FALLBACK: BINANCE_ORACLE, + CACHED: false, + }, +]; + +// TWT: promote RedStone to MAIN, Binance as PIVOT +export const NEW_TWT_ORACLE_CONFIG = { + NAME: "TWT", + ASSET: "0x4B0F1812e5Df2A09796481Ff14017e6005508003", + MAIN: REDSTONE_ORACLE, + PIVOT: BINANCE_ORACLE, + FALLBACK: addressZero, + CACHED: false, +}; + +export const NEW_ORACLE_CONFIG = [...NEW_STANDARD_ORACLE_CONFIG, NEW_TWT_ORACLE_CONFIG]; + +export const vip650 = () => { + const meta = { + version: "v2", + title: "VIP-650 [BNB Chain] Two-Vendor OEV Integration - RedStone Oracle Feed Expansion", + description: `**Description:** + +This proposal continues the Two-Vendor OEV Integration Framework adopted in [VIP-586](https://app.venus.io/#/governance/proposal/586) by expanding RedStone oracle coverage across the BSC Core Pool. + +It registers new RedStone price feeds for 13 additional assets and updates their resilient oracle configurations to strengthen price reliability through multi-vendor redundancy. + +**Actions:** + +- **Register new RedStone oracle feeds** for: XVS, LTC, BCH, DOT, LINK, DAI, FIL, DOGE, AAVE, UNI, FDUSD, TWT, SOL +- **Update resilient oracle configurations:** + - For XVS, LTC, BCH, DOT, LINK, DAI, FIL, DOGE, AAVE, UNI, FDUSD, SOL: Set MAIN=RedStone, PIVOT=Chainlink, FALLBACK=Binance + - For TWT: Set MAIN=RedStone, PIVOT=Binance +- **Set BoundValidator config** for TWT (required since TWT previously had no PIVOT oracle)`, + forDescription: "I agree that Venus Protocol should proceed with this proposal", + againstDescription: "I do not think that Venus Protocol should proceed with this proposal", + abstainDescription: "I am indifferent to whether Venus Protocol proceeds or not", + }; + + return makeProposal( + [ + /* ============ RedStone Oracle: Register new feeds ============ */ + + ...NEW_REDSTONE_ORACLE_FEEDS.map(feedData => { + return { + target: REDSTONE_ORACLE, + signature: "setTokenConfig((address,address,uint256))", + params: [[feedData.ASSET, feedData.FEED, feedData.MAX_STALE_PERIOD]], + }; + }), + + /* ============ Resilient Oracle: Update token configs ============ */ + + ...NEW_ORACLE_CONFIG.map(oracleData => { + return { + target: RESILIENT_ORACLE, + signature: "setTokenConfig((address,address[3],bool[3],bool))", + params: [ + [ + oracleData.ASSET, + [oracleData.MAIN, oracleData.PIVOT, oracleData.FALLBACK], + [oracleData.MAIN != addressZero, oracleData.PIVOT != addressZero, oracleData.FALLBACK != addressZero], + oracleData.CACHED, + ], + ], + }; + }), + + /* ============ BoundValidator: Set validation config for TWT ============ */ + + { + target: BOUND_VALIDATOR, + signature: "setValidateConfig((address,uint256,uint256))", + params: [[NEW_TWT_ORACLE_CONFIG.ASSET, PRICE_UPPER_BOUND, PRICE_LOWER_BOUND]], + }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip650;