diff --git a/REUSE.toml b/REUSE.toml index c39e4f6..0069026 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -15,6 +15,7 @@ path = [ "slither.config.json", "REUSE.toml", "audit/**/", + "canonical/**/", "foundry.lock", ] SPDX-FileCopyrightText = "Copyright (c) 2020 Rain Open Source Software Ltd" diff --git a/canonical/cyclo-site-tokens.json b/canonical/cyclo-site-tokens.json new file mode 100644 index 0000000..44c6c2a --- /dev/null +++ b/canonical/cyclo-site-tokens.json @@ -0,0 +1,199 @@ +{ + "tokens": [ + { + "active": true, + "chainId": 14, + "decimals": 18, + "name": "cysFLR", + "networkName": "Flare", + "receiptAddress": "0xd387fc43e19a63036d8fced559e81f5ddef7ef09", + "symbol": "cysFLR", + "underlyingAddress": "0x12e605bc104e93b45e1ad99f9e555f659051c2bb", + "underlyingDecimals": 18, + "underlyingSymbol": "sFLR", + "vaultAddress": "0x19831cfb53a0dbead9866c43557c1d48dff76567" + }, + { + "active": true, + "chainId": 14, + "decimals": 18, + "name": "Cyclo cyWETH", + "networkName": "Flare", + "receiptAddress": "0xbe2615a0fcb54a49a1eb472be30d992599fe0968", + "symbol": "cyWETH", + "underlyingAddress": "0x1502fa4be69d526124d453619276faccab275d3d", + "underlyingDecimals": 18, + "underlyingSymbol": "WETH", + "vaultAddress": "0xd8bf1d2720e9ffd01a2f9a2efc3e101a05b852b4" + }, + { + "active": true, + "chainId": 14, + "decimals": 6, + "name": "Cyclo cyFXRP.ftso (FTSO oracle)", + "networkName": "Flare", + "receiptAddress": "0xc46600cebd84ed2fe60ec525df13e341d24642f2", + "symbol": "cyFXRP.ftso", + "underlyingAddress": "0xad552a648c74d49e10027ab8a618a3ad4901c5be", + "underlyingDecimals": 6, + "underlyingSymbol": "FXRP", + "vaultAddress": "0xf23595ede14b54817397b1dab899ba061bdce7b5" + }, + { + "active": true, + "chainId": 42161, + "decimals": 18, + "name": "Cyclo cyWETH.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x0e67a81b967c189cf50353b0fe6fef572dc55319", + "symbol": "cyWETH.pyth", + "underlyingAddress": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "underlyingDecimals": 18, + "underlyingSymbol": "WETH", + "vaultAddress": "0x28c7747d7ea25ed3ddcd075c6ccc3634313a0f59" + }, + { + "active": true, + "chainId": 42161, + "decimals": 8, + "name": "Cyclo cyWBTC.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x922a293d4d0af30d67a51e5510a487916a2bb494", + "symbol": "cyWBTC.pyth", + "underlyingAddress": "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", + "underlyingDecimals": 8, + "underlyingSymbol": "WBTC", + "vaultAddress": "0x229917ac2842eaab42060a1a9213ca78e01b572a" + }, + { + "active": false, + "chainId": 42161, + "decimals": 8, + "name": "Cyclo cycbBTC.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x3a5ede5ae4ec55f61c4aff2cdfc920b5029abf05", + "symbol": "cycbBTC.pyth", + "underlyingAddress": "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf", + "underlyingDecimals": 8, + "underlyingSymbol": "cbBTC", + "vaultAddress": "0x9fc9da918552df0dad6c00051351e335656da100" + }, + { + "active": false, + "chainId": 42161, + "decimals": 18, + "name": "Cyclo cyLINK.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0xdf66e921c8c29e1b1ca729848790a4d0bd6cbde9", + "symbol": "cyLINK.pyth", + "underlyingAddress": "0xf97f4df75117a78c1a5a0dbb814af92458539fb4", + "underlyingDecimals": 18, + "underlyingSymbol": "LINK", + "vaultAddress": "0x715aa5f9a5b3c2b51c432c9028c8692029bce609" + }, + { + "active": false, + "chainId": 42161, + "decimals": 18, + "name": "Cyclo cyDOT.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x3b22b5ce7f9901fe6a676e57e079873775aaa331", + "symbol": "cyDOT.pyth", + "underlyingAddress": "0x8d010bf9c26881788b4e6bf5fd1bdc358c8f90b8", + "underlyingDecimals": 18, + "underlyingSymbol": "DOT", + "vaultAddress": "0xee6a7019679f96ced1ea861aae0c88d4481c7226" + }, + { + "active": false, + "chainId": 42161, + "decimals": 18, + "name": "Cyclo cyUNI.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0xbf979c720c730738e25d766748f7063f223f1d27", + "symbol": "cyUNI.pyth", + "underlyingAddress": "0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0", + "underlyingDecimals": 18, + "underlyingSymbol": "UNI", + "vaultAddress": "0x7cad3f864639738f9cc25952433cd844c07d16a4" + }, + { + "active": false, + "chainId": 42161, + "decimals": 18, + "name": "Cyclo cyPEPE.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0xdb2c91313aaaae40aedf6e91a1e78443241a64c0", + "symbol": "cyPEPE.pyth", + "underlyingAddress": "0x25d887ce7a35172c62febfd67a1856f20faebb00", + "underlyingDecimals": 18, + "underlyingSymbol": "PEPE", + "vaultAddress": "0x4dd4230f3b4d6118d905ed0b6f5f20a3b2472166" + }, + { + "active": false, + "chainId": 42161, + "decimals": 18, + "name": "Cyclo cyENA.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x7426ddc75b522e40552ea24d647898face0e2360", + "symbol": "cyENA.pyth", + "underlyingAddress": "0x58538e6a46e07434d7e7375bc268d3cb839c0133", + "underlyingDecimals": 18, + "underlyingSymbol": "ENA", + "vaultAddress": "0x5d938caf878bd56accf2b27fad9f697aa206df40" + }, + { + "active": true, + "chainId": 42161, + "decimals": 18, + "name": "Cyclo cyARB.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x3fee841c184dcf93f15cd28144b6e5514fffc18e", + "symbol": "cyARB.pyth", + "underlyingAddress": "0x912ce59144191c1204e64559fe8253a0e49e6548", + "underlyingDecimals": 18, + "underlyingSymbol": "ARB", + "vaultAddress": "0xc83563177290bdd391db56553ed828413b7689bc" + }, + { + "active": false, + "chainId": 42161, + "decimals": 18, + "name": "Cyclo cywstETH.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x8c1843a9f3278c94f6d79ceba9828596f524e898", + "symbol": "cywstETH.pyth", + "underlyingAddress": "0x5979d7b546e38e414f7e9822514be443a4800529", + "underlyingDecimals": 18, + "underlyingSymbol": "wstETH", + "vaultAddress": "0xc43ee790dc819db728e2c5bb6285359bbde7e016" + }, + { + "active": false, + "chainId": 42161, + "decimals": 6, + "name": "Cyclo cyXAUt0.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x45d63e1c4cb7201ed45d75af771bcfa9116a99be", + "symbol": "cyXAUt0.pyth", + "underlyingAddress": "0x40461291347e1ecbb09499f3371d3f17f10d7159", + "underlyingDecimals": 6, + "underlyingSymbol": "XAUt0", + "vaultAddress": "0x6ddc84f2fc195ae372501b01bb1ca25ca3221776" + }, + { + "active": false, + "chainId": 42161, + "decimals": 6, + "name": "Cyclo cyPYTH.pyth (Pyth oracle)", + "networkName": "Arbitrum One", + "receiptAddress": "0x93ac50b43aff21add1ad2a626cf6e88c5e00ea39", + "symbol": "cyPYTH.pyth", + "underlyingAddress": "0xe4d5c6ae46adfaf04313081e8c0052a30b6dd724", + "underlyingDecimals": 6, + "underlyingSymbol": "PYTH", + "vaultAddress": "0x87f7b55d7ccc9be93b0a8ae601801d79fa96fd4f" + } + ] +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 1bbcc20..adf070b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,6 +9,10 @@ optimizer_runs = 100000 # pre push0 for maximum compatibility evm_version = "paris" +# Canonical token registry shared with cyclofinance/cyclo.site. Tests in +# test/prod/CycloSiteTokens.prod.*.t.sol read it via vm.readFile. +fs_permissions = [{ access = "read", path = "./canonical" }] + remappings = [ "ethgild/=lib/ethgild/src/", "rain.factory/=lib/ethgild/lib/rain.factory/src/", diff --git a/test/lib/LibCycloSiteTokens.sol b/test/lib/LibCycloSiteTokens.sol new file mode 100644 index 0000000..6896a6b --- /dev/null +++ b/test/lib/LibCycloSiteTokens.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Vm} from "forge-std/Vm.sol"; +import {IERC20MetadataUpgradeable as IERC20Metadata} from + "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {IERC165Upgradeable as IERC165} from + "openzeppelin-contracts-upgradeable/contracts/utils/introspection/IERC165Upgradeable.sol"; +import {IERC1155Upgradeable as IERC1155} from + "openzeppelin-contracts-upgradeable/contracts/token/ERC1155/IERC1155Upgradeable.sol"; +import {CycloVault} from "src/concrete/vault/CycloVault.sol"; +import {IReceiptV3} from "ethgild/interface/IReceiptV3.sol"; +import {PROD_FLARE_VAULT_CYSFLR} from "src/lib/LibCycloProdVault.sol"; + +uint256 constant FLARE_CHAIN_ID = 14; +uint256 constant ARBITRUM_CHAIN_ID = 42161; + +string constant CYCLO_SITE_TOKENS_JSON_PATH = "./canonical/cyclo-site-tokens.json"; + +/// Mirrors `cyclofinance/cyclo.site:src/lib/stores.ts`'s `CyToken` shape. +/// Field order is alphabetical to match foundry's parseJson decoding rules. +struct TokenEntry { + bool active; + uint256 chainId; + uint256 decimals; + string name; + string networkName; + address receiptAddress; + string symbol; + address underlyingAddress; + uint256 underlyingDecimals; + string underlyingSymbol; + address vaultAddress; +} + +library LibCycloSiteTokens { + function loadAll(Vm vm) internal view returns (TokenEntry[] memory) { + string memory json = vm.readFile(CYCLO_SITE_TOKENS_JSON_PATH); + bytes memory data = vm.parseJson(json, ".tokens"); + return abi.decode(data, (TokenEntry[])); + } + + /// Asserts that for every entry on `expectedChainId`: + /// - the on-chain `block.chainid` matches `expectedChainId` (catches + /// mistakenly forking to the wrong chain) + /// - no two entries share the same `vaultAddress` or `receiptAddress` + /// - the entry's `networkName` matches `expectedNetworkName` + /// - `vaultAddress`, `underlyingAddress` and `receiptAddress` are each + /// non-zero and have bytecode + /// - the vault's on-chain `name()` matches `name` + /// - the vault's on-chain `decimals()` matches the JSON's `decimals` + /// - the underlying's on-chain `decimals()` matches `underlyingDecimals` + /// - the vault's on-chain `symbol()` matches `symbol` + /// - the underlying's on-chain `symbol()` matches `underlyingSymbol` + /// - `symbol` starts with `"cy" + underlyingSymbol` (the on-chain + /// `vault.symbol()` formula already enforces this transitively; + /// the explicit check guards the JSON-internal pairing) + /// - the vault's on-chain `asset()` matches the declared `underlyingAddress` + /// - the receipt supports the ERC1155 interface + /// - `receiptAddress.manager()` points back at the vault (works for every + /// entry, including `cysFLR`) + /// - the vault's on-chain `receipt()` matches the declared `receiptAddress`, + /// except for `cysFLR` whose older impl does not expose the getter + /// (tracked at cyclo.sol#43) + /// Reverts with the entry's `name` in the failure message so the operator + /// can locate the drift quickly. + function assertOnChainMatchesJson(Vm vm, uint256 expectedChainId, string memory expectedNetworkName) + internal + view + { + require(block.chainid == expectedChainId, "fork is on wrong chain"); + + TokenEntry[] memory entries = loadAll(vm); + uint256 verified = 0; + for (uint256 i = 0; i < entries.length; i++) { + TokenEntry memory entry = entries[i]; + if (entry.chainId != expectedChainId) continue; + verified++; + + for (uint256 j = i + 1; j < entries.length; j++) { + if (entries[j].chainId != expectedChainId) continue; + require( + entries[j].vaultAddress != entry.vaultAddress, + string.concat("duplicate vaultAddress for ", entry.name) + ); + require( + entries[j].receiptAddress != entry.receiptAddress, + string.concat("duplicate receiptAddress for ", entry.name) + ); + } + + require( + keccak256(bytes(entry.networkName)) == keccak256(bytes(expectedNetworkName)), + string.concat("networkName mismatch for ", entry.name) + ); + + require(entry.vaultAddress != address(0), string.concat("vaultAddress is zero for ", entry.name)); + require( + entry.vaultAddress.code.length > 0, + string.concat("vaultAddress has no bytecode for ", entry.name) + ); + + require( + entry.underlyingAddress != address(0), + string.concat("underlyingAddress is zero for ", entry.name) + ); + require( + entry.underlyingAddress.code.length > 0, + string.concat("underlyingAddress has no bytecode for ", entry.name) + ); + + require(entry.receiptAddress != address(0), string.concat("receiptAddress is zero for ", entry.name)); + require( + entry.receiptAddress.code.length > 0, + string.concat("receiptAddress has no bytecode for ", entry.name) + ); + + uint256 actualVaultDecimals = uint256(IERC20Metadata(entry.vaultAddress).decimals()); + require(actualVaultDecimals == entry.decimals, string.concat("vault decimals mismatch for ", entry.name)); + + uint256 actualUnderlyingDecimals = uint256(IERC20Metadata(entry.underlyingAddress).decimals()); + require( + actualUnderlyingDecimals == entry.underlyingDecimals, + string.concat("underlying decimals mismatch for ", entry.name) + ); + + string memory actualVaultSymbol = IERC20Metadata(entry.vaultAddress).symbol(); + require( + keccak256(bytes(actualVaultSymbol)) == keccak256(bytes(entry.symbol)), + string.concat("vault symbol mismatch for ", entry.name) + ); + + string memory actualUnderlyingSymbol = IERC20Metadata(entry.underlyingAddress).symbol(); + require( + keccak256(bytes(actualUnderlyingSymbol)) == keccak256(bytes(entry.underlyingSymbol)), + string.concat("underlying symbol mismatch for ", entry.name) + ); + + bytes memory symbolBytes = bytes(entry.symbol); + bytes memory expectedPrefix = bytes(string.concat("cy", entry.underlyingSymbol)); + require( + symbolBytes.length >= expectedPrefix.length, + string.concat("symbol shorter than 'cy' for ", entry.name) + ); + for (uint256 b = 0; b < expectedPrefix.length; b++) { + require( + symbolBytes[b] == expectedPrefix[b], + string.concat("symbol does not start with 'cy' for ", entry.name) + ); + } + + string memory actualVaultName = IERC20Metadata(entry.vaultAddress).name(); + require( + keccak256(bytes(actualVaultName)) == keccak256(bytes(entry.name)), + string.concat("vault name mismatch for ", entry.name) + ); + + address actualAsset = address(CycloVault(payable(entry.vaultAddress)).asset()); + require(actualAsset == entry.underlyingAddress, string.concat("vault.asset() mismatch for ", entry.name)); + + require( + IERC165(entry.receiptAddress).supportsInterface(type(IERC1155).interfaceId), + string.concat("receipt does not support ERC1155 for ", entry.name) + ); + + address receiptManager = IReceiptV3(entry.receiptAddress).manager(); + require( + receiptManager == entry.vaultAddress, + string.concat("receipt.manager() does not point at vault for ", entry.name) + ); + + if (entry.vaultAddress != PROD_FLARE_VAULT_CYSFLR) { + address actualReceipt = address(CycloVault(payable(entry.vaultAddress)).receipt()); + require( + actualReceipt == entry.receiptAddress, + string.concat("vault.receipt() mismatch for ", entry.name) + ); + } + } + require(verified > 0, "no JSON entries matched the requested chainId"); + } +} diff --git a/test/lib/LibCycloTestProd.sol b/test/lib/LibCycloTestProd.sol index e615554..8f11386 100644 --- a/test/lib/LibCycloTestProd.sol +++ b/test/lib/LibCycloTestProd.sol @@ -13,7 +13,7 @@ import {IERC20Upgradeable as IERC20} from uint256 constant PROD_TEST_BLOCK_NUMBER_FLARE = 51262162; -uint256 constant PROD_TEST_BLOCK_NUMBER_ARBITRUM = 455000000; +uint256 constant PROD_TEST_BLOCK_NUMBER_ARBITRUM = 460280886; string constant PROD_CYSFLR_RECEIPT_SYMBOL = "cysFLR RCPT"; string constant PROD_CYSFLR_RECEIPT_NAME = "cysFLR Receipt"; diff --git a/test/prod/CycloSiteTokens.prod.arbitrum.t.sol b/test/prod/CycloSiteTokens.prod.arbitrum.t.sol new file mode 100644 index 0000000..1529985 --- /dev/null +++ b/test/prod/CycloSiteTokens.prod.arbitrum.t.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {LibCycloTestProd} from "test/lib/LibCycloTestProd.sol"; +import {LibCycloSiteTokens, TokenEntry, ARBITRUM_CHAIN_ID} from "test/lib/LibCycloSiteTokens.sol"; +import {CycloVault, CycloVaultConfig} from "src/concrete/vault/CycloVault.sol"; +import { + PROD_ARBITRUM_CYCLO_VAULT_IMPLEMENTATION_V2, + PROD_ARBITRUM_CYCLO_VAULT_IMPLEMENTATION_V2_CODEHASH, + PROD_ARBITRUM_VAULT_CYWETH_PYTH, + PROD_ARBITRUM_VAULT_CYWSTETH_PYTH, + PROD_ARBITRUM_VAULT_CYWBTC_PYTH, + PROD_ARBITRUM_VAULT_CYCBBTC_PYTH, + PROD_ARBITRUM_VAULT_CYLINK_PYTH, + PROD_ARBITRUM_VAULT_CYDOT_PYTH, + PROD_ARBITRUM_VAULT_CYUNI_PYTH, + PROD_ARBITRUM_VAULT_CYPEPE_PYTH, + PROD_ARBITRUM_VAULT_CYPYTH_PYTH, + PROD_ARBITRUM_VAULT_CYENA_PYTH, + PROD_ARBITRUM_VAULT_CYARB_PYTH, + PROD_ARBITRUM_VAULT_CYXAUT_PYTH +} from "src/lib/LibCycloProdVault.sol"; +import { + PROD_ARBITRUM_CYCLO_RECEIPT_IMPLEMENTATION_V2, + PROD_ARBITRUM_CYCLO_RECEIPT_CODEHASH_V2, + PROD_ARBITRUM_RECEIPT_CYWETH_PYTH, + PROD_ARBITRUM_RECEIPT_CYWSTETH_PYTH, + PROD_ARBITRUM_RECEIPT_CYWBTC_PYTH, + PROD_ARBITRUM_RECEIPT_CYCBBTC_PYTH, + PROD_ARBITRUM_RECEIPT_CYLINK_PYTH, + PROD_ARBITRUM_RECEIPT_CYDOT_PYTH, + PROD_ARBITRUM_RECEIPT_CYUNI_PYTH, + PROD_ARBITRUM_RECEIPT_CYPEPE_PYTH, + PROD_ARBITRUM_RECEIPT_CYPYTH_PYTH, + PROD_ARBITRUM_RECEIPT_CYENA_PYTH, + PROD_ARBITRUM_RECEIPT_CYARB_PYTH, + PROD_ARBITRUM_RECEIPT_CYXAUT_PYTH +} from "src/lib/LibCycloProdReceipt.sol"; +import { + PROD_PYTH_ORACLE_WETH_USD_ARBITRUM, + PROD_PYTH_ORACLE_WSTETH_USD_ARBITRUM, + PROD_PYTH_ORACLE_WBTC_USD_ARBITRUM, + PROD_PYTH_ORACLE_CBBTC_USD_ARBITRUM, + PROD_PYTH_ORACLE_LINK_USD_ARBITRUM, + PROD_PYTH_ORACLE_DOT_USD_ARBITRUM, + PROD_PYTH_ORACLE_UNI_USD_ARBITRUM, + PROD_PYTH_ORACLE_PEPE_USD_ARBITRUM, + PROD_PYTH_ORACLE_PYTH_USD_ARBITRUM, + PROD_PYTH_ORACLE_ENA_USD_ARBITRUM, + PROD_PYTH_ORACLE_ARB_USD_ARBITRUM, + PROD_PYTH_ORACLE_XAUT_USD_ARBITRUM +} from "src/lib/LibCycloProdOracle.sol"; + +/// Reads `canonical/cyclo-site-tokens.json` and asserts every Arbitrum entry's +/// on-chain shape matches the JSON. Pins per-entry deployment details against +/// the prod constants in `LibCycloProd*.sol`: +/// - vaultAddress → expected vault impl + codehash (1167 proxy check) +/// - vaultAddress → expected priceOracle +/// - receiptAddress → expected receipt impl + codehash (1167 proxy check) +contract CycloSiteTokensProdArbitrumTest is Test { + mapping(address => bool) internal knownVaults; + mapping(address => bool) internal expectedActive; + mapping(address => address) internal expectedVaultImpl; + mapping(address => bytes32) internal expectedVaultCodehash; + mapping(address => address) internal expectedVaultOracle; + mapping(address => address) internal expectedReceiptImpl; + mapping(address => bytes32) internal expectedReceiptCodehash; + + function setUp() public { + LibCycloTestProd.createSelectForkArbitrum(vm); + + // Every Arbitrum cyToken vault is a 1167 proxy to the V2 impl. + address[12] memory vaults = [ + PROD_ARBITRUM_VAULT_CYWETH_PYTH, + PROD_ARBITRUM_VAULT_CYWSTETH_PYTH, + PROD_ARBITRUM_VAULT_CYWBTC_PYTH, + PROD_ARBITRUM_VAULT_CYCBBTC_PYTH, + PROD_ARBITRUM_VAULT_CYLINK_PYTH, + PROD_ARBITRUM_VAULT_CYDOT_PYTH, + PROD_ARBITRUM_VAULT_CYUNI_PYTH, + PROD_ARBITRUM_VAULT_CYPEPE_PYTH, + PROD_ARBITRUM_VAULT_CYPYTH_PYTH, + PROD_ARBITRUM_VAULT_CYENA_PYTH, + PROD_ARBITRUM_VAULT_CYARB_PYTH, + PROD_ARBITRUM_VAULT_CYXAUT_PYTH + ]; + for (uint256 i = 0; i < vaults.length; i++) { + knownVaults[vaults[i]] = true; + expectedVaultImpl[vaults[i]] = PROD_ARBITRUM_CYCLO_VAULT_IMPLEMENTATION_V2; + expectedVaultCodehash[vaults[i]] = PROD_ARBITRUM_CYCLO_VAULT_IMPLEMENTATION_V2_CODEHASH; + } + + expectedActive[PROD_ARBITRUM_VAULT_CYWETH_PYTH] = true; + expectedActive[PROD_ARBITRUM_VAULT_CYWBTC_PYTH] = true; + expectedActive[PROD_ARBITRUM_VAULT_CYARB_PYTH] = true; + + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYWETH_PYTH] = PROD_PYTH_ORACLE_WETH_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYWSTETH_PYTH] = PROD_PYTH_ORACLE_WSTETH_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYWBTC_PYTH] = PROD_PYTH_ORACLE_WBTC_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYCBBTC_PYTH] = PROD_PYTH_ORACLE_CBBTC_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYLINK_PYTH] = PROD_PYTH_ORACLE_LINK_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYDOT_PYTH] = PROD_PYTH_ORACLE_DOT_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYUNI_PYTH] = PROD_PYTH_ORACLE_UNI_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYPEPE_PYTH] = PROD_PYTH_ORACLE_PEPE_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYPYTH_PYTH] = PROD_PYTH_ORACLE_PYTH_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYENA_PYTH] = PROD_PYTH_ORACLE_ENA_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYARB_PYTH] = PROD_PYTH_ORACLE_ARB_USD_ARBITRUM; + expectedVaultOracle[PROD_ARBITRUM_VAULT_CYXAUT_PYTH] = PROD_PYTH_ORACLE_XAUT_USD_ARBITRUM; + + // Every Arbitrum receipt is a 1167 proxy to the V2 receipt impl. + address[12] memory receipts = [ + PROD_ARBITRUM_RECEIPT_CYWETH_PYTH, + PROD_ARBITRUM_RECEIPT_CYWSTETH_PYTH, + PROD_ARBITRUM_RECEIPT_CYWBTC_PYTH, + PROD_ARBITRUM_RECEIPT_CYCBBTC_PYTH, + PROD_ARBITRUM_RECEIPT_CYLINK_PYTH, + PROD_ARBITRUM_RECEIPT_CYDOT_PYTH, + PROD_ARBITRUM_RECEIPT_CYUNI_PYTH, + PROD_ARBITRUM_RECEIPT_CYPEPE_PYTH, + PROD_ARBITRUM_RECEIPT_CYPYTH_PYTH, + PROD_ARBITRUM_RECEIPT_CYENA_PYTH, + PROD_ARBITRUM_RECEIPT_CYARB_PYTH, + PROD_ARBITRUM_RECEIPT_CYXAUT_PYTH + ]; + for (uint256 i = 0; i < receipts.length; i++) { + expectedReceiptImpl[receipts[i]] = PROD_ARBITRUM_CYCLO_RECEIPT_IMPLEMENTATION_V2; + expectedReceiptCodehash[receipts[i]] = PROD_ARBITRUM_CYCLO_RECEIPT_CODEHASH_V2; + } + } + + function testCycloSiteTokensArbitrum() external { + LibCycloSiteTokens.assertOnChainMatchesJson(vm, ARBITRUM_CHAIN_ID, "Arbitrum One"); + + TokenEntry[] memory entries = LibCycloSiteTokens.loadAll(vm); + uint256 chainEntries = 0; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].chainId == ARBITRUM_CHAIN_ID) chainEntries++; + } + require(chainEntries == 12, "expected exactly 12 Arbitrum entries in JSON"); + + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].chainId != ARBITRUM_CHAIN_ID) continue; + TokenEntry memory entry = entries[i]; + + require( + knownVaults[entry.vaultAddress], + string.concat("JSON vaultAddress not a known Arbitrum prod constant for ", entry.name) + ); + + require( + entry.active == expectedActive[entry.vaultAddress], + string.concat("active flag mismatch for ", entry.name) + ); + + LibCycloTestProd.checkCBORTrimmedBytecodeHashBy1167Proxy( + entry.vaultAddress, expectedVaultImpl[entry.vaultAddress], expectedVaultCodehash[entry.vaultAddress] + ); + + require( + address(CycloVault(payable(entry.vaultAddress)).priceOracle()) + == expectedVaultOracle[entry.vaultAddress], + string.concat("priceOracle mismatch for ", entry.name) + ); + + require( + expectedReceiptImpl[entry.receiptAddress] != address(0), + string.concat("receiptAddress not a known Arbitrum prod constant for ", entry.name) + ); + LibCycloTestProd.checkCBORTrimmedBytecodeHashBy1167Proxy( + entry.receiptAddress, + expectedReceiptImpl[entry.receiptAddress], + expectedReceiptCodehash[entry.receiptAddress] + ); + + // Vaults take a `CycloVaultConfig` struct, receipts take an + // address (the manager). With well-formed data the OZ + // `initializer` modifier fires before the body decodes data, + // producing the canonical "already initialized" revert string. + CycloVaultConfig memory vaultConfig; + LibCycloTestProd.checkIsInitialized(vm, entry.vaultAddress, abi.encode(vaultConfig)); + LibCycloTestProd.checkIsInitialized(vm, entry.receiptAddress, abi.encode(entry.vaultAddress)); + } + + // Reverse coverage: every prod vault constant must appear in the JSON. + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYWETH_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYWSTETH_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYWBTC_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYCBBTC_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYLINK_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYDOT_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYUNI_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYPEPE_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYPYTH_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYENA_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYARB_PYTH); + _assertProdConstantInJson(entries, PROD_ARBITRUM_VAULT_CYXAUT_PYTH); + } + + function _assertProdConstantInJson(TokenEntry[] memory entries, address vault) internal pure { + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].vaultAddress == vault) return; + } + revert("prod vault constant missing from JSON"); + } +} diff --git a/test/prod/CycloSiteTokens.prod.flare.t.sol b/test/prod/CycloSiteTokens.prod.flare.t.sol new file mode 100644 index 0000000..f27bfa8 --- /dev/null +++ b/test/prod/CycloSiteTokens.prod.flare.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {LibCycloTestProd} from "test/lib/LibCycloTestProd.sol"; +import {CycloVaultConfig} from "src/concrete/vault/CycloVault.sol"; +import {LibCycloSiteTokens, TokenEntry, FLARE_CHAIN_ID} from "test/lib/LibCycloSiteTokens.sol"; +import {CycloVault} from "src/concrete/vault/CycloVault.sol"; +import { + PROD_FLARE_VAULT_IMPLEMENTATION_CYSFLR, + PROD_FLARE_VAULT_IMPLEMENTATION_CYSFLR_CODEHASH, + PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V1, + PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V1_CODEHASH, + PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V2, + PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V2_CODEHASH, + PROD_FLARE_VAULT_CYSFLR, + PROD_FLARE_VAULT_CYWETH, + PROD_FLARE_VAULT_CYFXRP, + PROD_FLARE_VAULT_CYJOULE +} from "src/lib/LibCycloProdVault.sol"; +import { + PROD_FLARE_RECEIPT_IMPLEMENTATION_CYSFLR, + PROD_FLARE_RECEIPT_IMPLEMENTATION_CYSFLR_CODEHASH, + PROD_FLARE_CYCLO_RECEIPT_IMPLEMENTATION_V1, + PROD_FLARE_CYCLO_RECEIPT_CODEHASH_V1, + PROD_FLARE_CYCLO_RECEIPT_IMPLEMENTATION_V2, + PROD_FLARE_CYCLO_RECEIPT_CODEHASH_V2, + PROD_FLARE_RECEIPT_CYSFLR, + PROD_FLARE_RECEIPT_CYWETH, + PROD_FLARE_RECEIPT_CYFXRP +} from "src/lib/LibCycloProdReceipt.sol"; +import { + PROD_FLARE_TWO_PRICE_ORACLE_FLR_USD__SFLR_V2, + PROD_FLARE_FTSO_V2_LTS_ETH_USD_FEED_ORACLE, + PROD_FLARE_FTSO_V2_LTS_XRP_USD_FEED_ORACLE +} from "src/lib/LibCycloProdOracle.sol"; + +/// Reads `canonical/cyclo-site-tokens.json` and asserts every Flare entry's +/// on-chain shape matches the JSON. Pins per-entry deployment details against +/// the prod constants in `LibCycloProd*.sol`: +/// - vaultAddress → expected vault impl + codehash (1167 proxy check) +/// - vaultAddress → expected priceOracle +/// - receiptAddress → expected receipt impl + codehash (1167 proxy check) +contract CycloSiteTokensProdFlareTest is Test { + mapping(address => bool) internal knownVaults; + mapping(address => bool) internal expectedActive; + mapping(address => address) internal expectedVaultImpl; + mapping(address => bytes32) internal expectedVaultCodehash; + mapping(address => address) internal expectedVaultOracle; + mapping(address => address) internal expectedReceiptImpl; + mapping(address => bytes32) internal expectedReceiptCodehash; + + function setUp() public { + LibCycloTestProd.createSelectForkFlare(vm); + + knownVaults[PROD_FLARE_VAULT_CYSFLR] = true; + knownVaults[PROD_FLARE_VAULT_CYWETH] = true; + knownVaults[PROD_FLARE_VAULT_CYFXRP] = true; + knownVaults[PROD_FLARE_VAULT_CYJOULE] = true; + + expectedActive[PROD_FLARE_VAULT_CYSFLR] = true; + expectedActive[PROD_FLARE_VAULT_CYWETH] = true; + expectedActive[PROD_FLARE_VAULT_CYFXRP] = true; + + expectedVaultImpl[PROD_FLARE_VAULT_CYSFLR] = PROD_FLARE_VAULT_IMPLEMENTATION_CYSFLR; + expectedVaultCodehash[PROD_FLARE_VAULT_CYSFLR] = PROD_FLARE_VAULT_IMPLEMENTATION_CYSFLR_CODEHASH; + expectedVaultImpl[PROD_FLARE_VAULT_CYWETH] = PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V1; + expectedVaultCodehash[PROD_FLARE_VAULT_CYWETH] = PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V1_CODEHASH; + expectedVaultImpl[PROD_FLARE_VAULT_CYFXRP] = PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V2; + expectedVaultCodehash[PROD_FLARE_VAULT_CYFXRP] = PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V2_CODEHASH; + expectedVaultImpl[PROD_FLARE_VAULT_CYJOULE] = PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V2; + expectedVaultCodehash[PROD_FLARE_VAULT_CYJOULE] = PROD_FLARE_CYCLO_VAULT_IMPLEMENTATION_V2_CODEHASH; + + expectedVaultOracle[PROD_FLARE_VAULT_CYSFLR] = PROD_FLARE_TWO_PRICE_ORACLE_FLR_USD__SFLR_V2; + expectedVaultOracle[PROD_FLARE_VAULT_CYWETH] = PROD_FLARE_FTSO_V2_LTS_ETH_USD_FEED_ORACLE; + expectedVaultOracle[PROD_FLARE_VAULT_CYFXRP] = PROD_FLARE_FTSO_V2_LTS_XRP_USD_FEED_ORACLE; + + expectedReceiptImpl[PROD_FLARE_RECEIPT_CYSFLR] = PROD_FLARE_RECEIPT_IMPLEMENTATION_CYSFLR; + expectedReceiptCodehash[PROD_FLARE_RECEIPT_CYSFLR] = PROD_FLARE_RECEIPT_IMPLEMENTATION_CYSFLR_CODEHASH; + expectedReceiptImpl[PROD_FLARE_RECEIPT_CYWETH] = PROD_FLARE_CYCLO_RECEIPT_IMPLEMENTATION_V1; + expectedReceiptCodehash[PROD_FLARE_RECEIPT_CYWETH] = PROD_FLARE_CYCLO_RECEIPT_CODEHASH_V1; + expectedReceiptImpl[PROD_FLARE_RECEIPT_CYFXRP] = PROD_FLARE_CYCLO_RECEIPT_IMPLEMENTATION_V2; + expectedReceiptCodehash[PROD_FLARE_RECEIPT_CYFXRP] = PROD_FLARE_CYCLO_RECEIPT_CODEHASH_V2; + } + + function testCycloSiteTokensFlare() external { + LibCycloSiteTokens.assertOnChainMatchesJson(vm, FLARE_CHAIN_ID, "Flare"); + + TokenEntry[] memory entries = LibCycloSiteTokens.loadAll(vm); + uint256 chainEntries = 0; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].chainId == FLARE_CHAIN_ID) chainEntries++; + } + require(chainEntries == 3, "expected exactly 3 Flare entries in JSON"); + + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].chainId != FLARE_CHAIN_ID) continue; + TokenEntry memory entry = entries[i]; + + require( + knownVaults[entry.vaultAddress], + string.concat("JSON vaultAddress not a known Flare prod constant for ", entry.name) + ); + + require( + entry.active == expectedActive[entry.vaultAddress], + string.concat("active flag mismatch for ", entry.name) + ); + + LibCycloTestProd.checkCBORTrimmedBytecodeHashBy1167Proxy( + entry.vaultAddress, expectedVaultImpl[entry.vaultAddress], expectedVaultCodehash[entry.vaultAddress] + ); + + require( + address(CycloVault(payable(entry.vaultAddress)).priceOracle()) + == expectedVaultOracle[entry.vaultAddress], + string.concat("priceOracle mismatch for ", entry.name) + ); + + require( + expectedReceiptImpl[entry.receiptAddress] != address(0), + string.concat("receiptAddress not a known Flare prod constant for ", entry.name) + ); + LibCycloTestProd.checkCBORTrimmedBytecodeHashBy1167Proxy( + entry.receiptAddress, + expectedReceiptImpl[entry.receiptAddress], + expectedReceiptCodehash[entry.receiptAddress] + ); + + // Vaults take a `CycloVaultConfig` struct, receipts take an + // address (the manager). With well-formed data the OZ + // `initializer` modifier fires before the body decodes data, + // producing the canonical "already initialized" revert string. + CycloVaultConfig memory vaultConfig; + LibCycloTestProd.checkIsInitialized(vm, entry.vaultAddress, abi.encode(vaultConfig)); + LibCycloTestProd.checkIsInitialized(vm, entry.receiptAddress, abi.encode(entry.vaultAddress)); + } + + // Reverse coverage: every prod vault constant must appear in the JSON, + // unless explicitly excluded. cyJOULE has been retired from the cyclo.site + // listing but its constant is kept for historical bytecode tests. + _assertProdConstantInJson(entries, PROD_FLARE_VAULT_CYSFLR); + _assertProdConstantInJson(entries, PROD_FLARE_VAULT_CYWETH); + _assertProdConstantInJson(entries, PROD_FLARE_VAULT_CYFXRP); + // PROD_FLARE_VAULT_CYJOULE — intentionally excluded from JSON. + } + + function _assertProdConstantInJson(TokenEntry[] memory entries, address vault) internal pure { + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].vaultAddress == vault) return; + } + revert("prod vault constant missing from JSON"); + } +} diff --git a/test/prod/CycloVault.prod.arbitrum.t.sol b/test/prod/CycloVault.prod.arbitrum.t.sol index 7691d6e..d16973c 100644 --- a/test/prod/CycloVault.prod.arbitrum.t.sol +++ b/test/prod/CycloVault.prod.arbitrum.t.sol @@ -56,6 +56,7 @@ import {PROD_ARBITRUM_CLONE_FACTORY_ADDRESS_V1} from "src/lib/LibCycloProdCloneF import {IERC20MetadataUpgradeable as IERC20Metadata} from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import {IReceiptV3} from "ethgild/abstract/ReceiptVault.sol"; +import {IPriceOracleV2} from "ethgild/interface/IPriceOracleV2.sol"; contract CycloVaultProdArbitrumTest is CycloVaultTest { function _rpcEnvName() internal pure override returns (string memory) { @@ -286,69 +287,59 @@ contract CycloVaultProdArbitrumTest is CycloVaultTest { LibCycloTestProd.checkDeposit(vm, PROD_ARBITRUM_VAULT_CYXAUT_PYTH, deposit); } + /// Mints `shares` from `vault` if its Pyth feed is fresh at the pinned + /// block. If the feed is stale, asserts the full revert chain we observe: + /// `previewMint` panics with divide-by-zero (0x12) because + /// `ERC20PriceOracleReceiptVault._nextId()` swallows the oracle revert in a + /// try/catch and returns id=0, then `_calculateMint` divides by zero. The + /// underlying cause is pinned by calling the oracle directly and asserting + /// `StalePrice()` (Pyth selector 0x19abf40e). + function _checkMintForVault(address vaultAddress, address oracleAddress, uint256 shares) internal { + CycloVault vault = CycloVault(payable(vaultAddress)); + try vault.previewMint(shares, 0) returns (uint256 expectedAssets) { + deal(vault.asset(), DEFAULT_ALICE, expectedAssets); + LibCycloTestProd.checkMint(vm, vaultAddress, shares, expectedAssets); + } catch (bytes memory err) { + require( + keccak256(err) == keccak256(abi.encodeWithSignature("Panic(uint256)", uint256(0x12))), + "previewMint reverted with something other than divide-by-zero" + ); + vm.expectRevert(abi.encodeWithSignature("StalePrice()")); + IPriceOracleV2(payable(oracleAddress)).price(); + } + } + + /// Pins the swallow chain in `ERC20PriceOracleReceiptVault._nextId()` + /// against the cyWETH vault, independently of whether its Pyth feed is + /// stale at the pinned block. Mocks the oracle's `price()` to revert + /// with `StalePrice()` and asserts `previewMint` surfaces `Panic(0x12)`. + function testProdCycloVaultMintRevertsOnStalePythSwallowArbitrum() external { + vm.mockCallRevert( + PROD_PYTH_ORACLE_WETH_USD_ARBITRUM, + abi.encodeWithSignature("price()"), + abi.encodeWithSignature("StalePrice()") + ); + + vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", uint256(0x12))); + CycloVault(payable(PROD_ARBITRUM_VAULT_CYWETH_PYTH)).previewMint(1, 0); + } + /// forge-config: default.fuzz.runs = 1 function testProdCycloVaultCanMintArbitrum(uint256 sharesSeed) public { uint256 shares = bound(sharesSeed, 1, type(uint128).max); - CycloVault vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYWETH_PYTH)); - uint256 assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYWETH_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYWSTETH_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYWSTETH_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYWBTC_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYWBTC_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYCBBTC_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYCBBTC_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYLINK_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYLINK_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYDOT_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYDOT_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYUNI_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYUNI_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYPEPE_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYPEPE_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYPYTH_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYPYTH_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYENA_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYENA_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYARB_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYARB_PYTH, shares, assets); - - vault = CycloVault(payable(PROD_ARBITRUM_VAULT_CYXAUT_PYTH)); - assets = vault.previewMint(shares, 0); - deal(vault.asset(), DEFAULT_ALICE, assets); - LibCycloTestProd.checkMint(vm, PROD_ARBITRUM_VAULT_CYXAUT_PYTH, shares, assets); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYWETH_PYTH, PROD_PYTH_ORACLE_WETH_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYWSTETH_PYTH, PROD_PYTH_ORACLE_WSTETH_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYWBTC_PYTH, PROD_PYTH_ORACLE_WBTC_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYCBBTC_PYTH, PROD_PYTH_ORACLE_CBBTC_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYLINK_PYTH, PROD_PYTH_ORACLE_LINK_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYDOT_PYTH, PROD_PYTH_ORACLE_DOT_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYUNI_PYTH, PROD_PYTH_ORACLE_UNI_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYPEPE_PYTH, PROD_PYTH_ORACLE_PEPE_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYPYTH_PYTH, PROD_PYTH_ORACLE_PYTH_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYENA_PYTH, PROD_PYTH_ORACLE_ENA_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYARB_PYTH, PROD_PYTH_ORACLE_ARB_USD_ARBITRUM, shares); + _checkMintForVault(PROD_ARBITRUM_VAULT_CYXAUT_PYTH, PROD_PYTH_ORACLE_XAUT_USD_ARBITRUM, shares); } function testProdCycloVaultImplementationIsInitializedArbitrum() external { diff --git a/test/prod/PythOracle.prod.t.sol b/test/prod/PythOracle.prod.t.sol index da86859..68029bd 100644 --- a/test/prod/PythOracle.prod.t.sol +++ b/test/prod/PythOracle.prod.t.sol @@ -37,44 +37,50 @@ import {LibCycloTestProd} from "test/lib/LibCycloTestProd.sol"; import {IPriceOracleV2} from "ethgild/interface/IPriceOracleV2.sol"; contract PythOracleProdTest is Test { + /// At the pinned block, WBTC, UNI and XAUT have fresh Pyth pushes; the + /// other 9 feeds are older than the 1800s stale threshold and revert with + /// `StalePrice()` (selector 0x19abf40e). The fresh prices are pinned + /// exactly; the stale ones are pinned as `vm.expectRevert(StalePrice)`. function testProdCycloPythOraclePrice() external { LibCycloTestProd.createSelectForkArbitrum(vm); - uint256 price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_WETH_USD_ARBITRUM)).price(); - assertEq(price, 2904.76083912e18); + bytes memory stalePrice = abi.encodeWithSignature("StalePrice()"); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_WSTETH_USD_ARBITRUM)).price(); - assertEq(price, 3550.61377701e18); + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_WETH_USD_ARBITRUM)).price(); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_WBTC_USD_ARBITRUM)).price(); - assertEq(price, 86560.66805924e18); + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_WSTETH_USD_ARBITRUM)).price(); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_CBBTC_USD_ARBITRUM)).price(); - assertEq(price, 86785.63466162e18); + uint256 price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_WBTC_USD_ARBITRUM)).price(); + assertEq(price, 80906.60000001e18); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_LINK_USD_ARBITRUM)).price(); - assertEq(price, 12.80597072e18); + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_CBBTC_USD_ARBITRUM)).price(); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_DOT_USD_ARBITRUM)).price(); - assertEq(price, 2.24827361e18); + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_LINK_USD_ARBITRUM)).price(); + + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_DOT_USD_ARBITRUM)).price(); price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_UNI_USD_ARBITRUM)).price(); - assertEq(price, 6.00215163e18); + assertEq(price, 3.48202396e18); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_PEPE_USD_ARBITRUM)).price(); - assertEq(price, 0.0000044354e18); + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_PEPE_USD_ARBITRUM)).price(); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_ENA_USD_ARBITRUM)).price(); - assertEq(price, 0.27290774e18); + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_ENA_USD_ARBITRUM)).price(); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_ARB_USD_ARBITRUM)).price(); - assertEq(price, 0.20872191e18); + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_ARB_USD_ARBITRUM)).price(); - price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_PYTH_USD_ARBITRUM)).price(); - assertEq(price, 0.0749194e18); + vm.expectRevert(stalePrice); + IPriceOracleV2(payable(PROD_PYTH_ORACLE_PYTH_USD_ARBITRUM)).price(); price = IPriceOracleV2(payable(PROD_PYTH_ORACLE_XAUT_USD_ARBITRUM)).price(); - assertEq(price, 4140.27469615e18); + assertEq(price, 4711.38445871e18); } function testProdCycloPythOracleBytecode() external {