From 5fdb4f3fe0ea3a86118252c009b726abca1a9769 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Wed, 6 May 2026 15:11:45 +0200 Subject: [PATCH 01/11] fix: implement CYS3-03 fix --- script/Deploy.s.sol | 1 + script/UpgradeBase.s.sol | 21 +- script/UpgradeClaimManager.s.sol | 19 +- script/VerifySepoliaConfig.s.sol | 16 +- src/ClaimManager.sol | 179 +++++++- src/interfaces/IChainlinkPriceFeed.sol | 15 + src/interfaces/IClaimManager.sol | 87 +++- test/defi/mocks/CoverageMocks.sol | 8 + test/fuzz/ClaimManager.t.sol | 19 + test/unit/ClaimManager.t.sol | 598 ++++++++++++++++++++++--- 10 files changed, 865 insertions(+), 98 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 1b6ba003..cb2ce7a9 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -427,6 +427,7 @@ abstract contract Deploy is CreateXDeployer { ); ClaimManager(payable(contracts.claimManagerProxy)).setPremiumManager(contracts.premiumManagerProxy); + ClaimManager(payable(contracts.claimManagerProxy)).initializeV2(external_.chainlinkPriceFeed, 300); PolicyManager(contracts.policyManagerProxy).setClaimManager(contracts.claimManagerProxy); diff --git a/script/UpgradeBase.s.sol b/script/UpgradeBase.s.sol index 0d5ce32c..b99ac160 100644 --- a/script/UpgradeBase.s.sol +++ b/script/UpgradeBase.s.sol @@ -73,6 +73,15 @@ abstract contract UpgradeBase is Script { */ function contractName() internal pure virtual returns (string memory); + /** + * @notice Returns optional calldata to pass to `upgradeToAndCall` for initializing new storage. + * @dev Override in subclasses that introduce new state variables via a reinitializer (e.g. initializeV2). + * Returning empty bytes causes a plain `upgradeProxy` without any call (backward-compatible default). + */ + function upgradeCalldata() internal view virtual returns (bytes memory) { + return ""; + } + /** * @notice Upgrades the proxy to a new implementation. * @dev Validates chain id, loads proxy address from environment, checks admin role, @@ -124,7 +133,7 @@ abstract contract UpgradeBase is Script { opts.unsafeAllow = "constructor"; opts.unsafeSkipStorageCheck = true; - Upgrades.upgradeProxy(result.proxy, contractArtifact(), "", opts); + Upgrades.upgradeProxy(result.proxy, contractArtifact(), upgradeCalldata(), opts); vm.stopBroadcast(); @@ -164,17 +173,19 @@ abstract contract UpgradeBase is Script { result.newImplementation = newImpl; // Build the upgradeToAndCall calldata that the timelock will execute on the proxy - bytes memory upgradeCalldata = abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (newImpl, "")); + bytes memory timelockUpgradeCalldata = + abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (newImpl, upgradeCalldata())); uint256 minDelay = TimelockController(payable(timelock)).getMinDelay(); // Build TimelockController.schedule() calldata bytes memory scheduleCalldata = abi.encodeCall( - TimelockController.schedule, (result.proxy, 0, upgradeCalldata, bytes32(0), bytes32(0), minDelay) + TimelockController.schedule, (result.proxy, 0, timelockUpgradeCalldata, bytes32(0), bytes32(0), minDelay) ); // Build TimelockController.execute() calldata - bytes memory executeCalldata = - abi.encodeCall(TimelockController.execute, (result.proxy, 0, upgradeCalldata, bytes32(0), bytes32(0))); + bytes memory executeCalldata = abi.encodeCall( + TimelockController.execute, (result.proxy, 0, timelockUpgradeCalldata, bytes32(0), bytes32(0)) + ); _logUpgrade(config, timelock, result, contractName()); diff --git a/script/UpgradeClaimManager.s.sol b/script/UpgradeClaimManager.s.sol index c0c41c17..0a1ac0b9 100644 --- a/script/UpgradeClaimManager.s.sol +++ b/script/UpgradeClaimManager.s.sol @@ -1,12 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {ClaimManager} from "../src/ClaimManager.sol"; import {UpgradeBase} from "./UpgradeBase.s.sol"; /** * @title UpgradeClaimManager * @notice Shared upgrade logic for ClaimManager UUPS proxy upgrades. - * @dev Executes an upgrade via OpenZeppelin's Upgrades library. + * @dev Executes an upgrade via OpenZeppelin's Upgrades library and atomically calls + * ClaimManager.initializeV2() via upgradeToAndCall to wire the new V2 storage fields. * * WARNING: Storage layout checks are skipped. Manually verify layout before * upgrading to avoid corrupting proxy state. @@ -14,6 +16,9 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * Environment Variables: * - PRIVATE_KEY: Deployer private key with DEFAULT_ADMIN_ROLE on ClaimManager * - CLAIM_MANAGER_PROXY: Address of the deployed ClaimManager proxy (required) + * - CHAINLINK_PRICE_FEED: Address of the ChainlinkPriceFeed contract (required) + * - PRICE_DEVIATION_TOLERANCE_BPS: Max DEX-vs-Chainlink deviation tolerance in bps + * (optional, defaults to 300 = 3%) */ abstract contract UpgradeClaimManager is UpgradeBase { /** @@ -34,4 +39,16 @@ abstract contract UpgradeClaimManager is UpgradeBase { function contractName() internal pure override returns (string memory) { return "ClaimManager"; } + + /** + * @notice Returns initializeV2 calldata for the ClaimManager upgrade. + * @dev Encoded as the data argument to upgradeToAndCall so the proxy atomically configures + * the V2 storage fields in the same transaction as the implementation swap. + */ + function upgradeCalldata() internal view override returns (bytes memory) { + address chainlinkPriceFeed = vm.envAddress("CHAINLINK_PRICE_FEED"); + require(chainlinkPriceFeed != address(0), "CHAINLINK_PRICE_FEED required"); + uint16 toleranceBps = uint16(vm.envOr("PRICE_DEVIATION_TOLERANCE_BPS", uint256(300))); + return abi.encodeCall(ClaimManager.initializeV2, (chainlinkPriceFeed, toleranceBps)); + } } diff --git a/script/VerifySepoliaConfig.s.sol b/script/VerifySepoliaConfig.s.sol index da1c7b04..4e7d1fda 100644 --- a/script/VerifySepoliaConfig.s.sol +++ b/script/VerifySepoliaConfig.s.sol @@ -53,6 +53,8 @@ interface IPolicyManagerView { interface IClaimManagerView { function slashingManager() external view returns (address); function premiumManager() external view returns (address); + function chainlinkPriceFeed() external view returns (address); + function priceDeviationToleranceBps() external view returns (uint16); } interface IPremiumManagerView { @@ -141,7 +143,7 @@ contract VerifySepoliaConfig is Script { console2.log(""); if (_failures == 0) { - console2.log("[ALL OK] All", uint256(25), "checks passed. Configuration is complete for E2E testing."); + console2.log("[ALL OK] All", uint256(27), "checks passed. Configuration is complete for E2E testing."); } else { console2.log("[INCOMPLETE]", _failures, "check(s) failed. See [FAIL] lines above."); console2.log(" Fix: run cast send commands in docs/deployment/CoreAdminActions-Sepolia.md"); @@ -278,6 +280,18 @@ contract VerifySepoliaConfig is Script { _check("ClaimManager.slashingManager", IClaimManagerView(CLAIM_MANAGER).slashingManager(), SLASHING_MANAGER); _check("ClaimManager.premiumManager", IClaimManagerView(CLAIM_MANAGER).premiumManager(), PREMIUM_MANAGER); + _check( + "ClaimManager.chainlinkPriceFeed", + IClaimManagerView(CLAIM_MANAGER).chainlinkPriceFeed(), + CHAINLINK_PRICE_FEED + ); + uint16 toleranceBps = IClaimManagerView(CLAIM_MANAGER).priceDeviationToleranceBps(); + if (toleranceBps > 0) { + console2.log("[OK] ClaimManager.priceDeviationToleranceBps:", toleranceBps, "bps"); + } else { + console2.log("[FAIL] ClaimManager.priceDeviationToleranceBps is 0 -- initializeV2 not called?"); + _failures++; + } _checkRole( "PremiumManager: CLAIM_MANAGER_ROLE -> ClaimManager", PREMIUM_MANAGER, diff --git a/src/ClaimManager.sol b/src/ClaimManager.sol index 8cf34286..2afb7bab 100644 --- a/src/ClaimManager.sol +++ b/src/ClaimManager.sol @@ -9,6 +9,7 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IChainlinkPriceFeed} from "./interfaces/IChainlinkPriceFeed.sol"; import {IClaimManager} from "./interfaces/IClaimManager.sol"; import {IPolicyManager} from "./interfaces/IPolicyManager.sol"; import {IPremiumManager} from "./interfaces/IPremiumManager.sol"; @@ -47,6 +48,8 @@ contract ClaimManager is uint16 private constant _MAX_SLIPPAGE_BPS = 10_000; /// @dev Upper bound for the admin-settable slippage parameter. uint16 private constant _MAX_SETTABLE_SLIPPAGE_BPS = 3000; + /// @dev Upper bound for the admin-settable price deviation tolerance. + uint16 private constant _MAX_SETTABLE_DEVIATION_BPS = 1000; address private constant _NATIVE_ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint32 private constant _SWAP_DEADLINE_BUFFER = 15 minutes; @@ -62,6 +65,12 @@ contract ClaimManager is mapping(uint96 policyId => bool) private _claimApprovalRequired; mapping(uint96 policyId => mapping(bytes32 evidenceHash => bool used)) private _usedEvidenceHashes; + // --- Storage layout V2 (appended; do not insert variables above this line) --- + address public override chainlinkPriceFeed; + uint16 public override priceDeviationToleranceBps; + // slither-disable-next-line unused-state + uint256[48] private __gap; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -108,6 +117,20 @@ contract ClaimManager is maxSwapSlippageBps = 200; // 2% } + /** + * @inheritdoc IClaimManager + */ + function initializeV2(address chainlinkPriceFeed_, uint16 priceDeviationToleranceBps_) external reinitializer(2) { + require(chainlinkPriceFeed_ != address(0), ZeroAddress()); + require( + priceDeviationToleranceBps_ <= _MAX_SETTABLE_DEVIATION_BPS, InvalidDeviationBps(priceDeviationToleranceBps_) + ); + chainlinkPriceFeed = chainlinkPriceFeed_; + priceDeviationToleranceBps = priceDeviationToleranceBps_; + emit ChainlinkPriceFeedSet(chainlinkPriceFeed_); + emit PriceDeviationToleranceBpsSet(priceDeviationToleranceBps_); + } + /** * @inheritdoc IClaimManager */ @@ -176,6 +199,30 @@ contract ClaimManager is emit PremiumManagerSet(premiumManager_); } + /** + * @inheritdoc IClaimManager + */ + function setChainlinkPriceFeed(address chainlinkPriceFeed_) external override onlyRole(DEFAULT_ADMIN_ROLE) { + require(chainlinkPriceFeed_ != address(0), ZeroAddress()); + chainlinkPriceFeed = chainlinkPriceFeed_; + emit ChainlinkPriceFeedSet(chainlinkPriceFeed_); + } + + /** + * @inheritdoc IClaimManager + */ + function setPriceDeviationToleranceBps(uint16 priceDeviationToleranceBps_) + external + override + onlyRole(DEFAULT_ADMIN_ROLE) + { + require( + priceDeviationToleranceBps_ <= _MAX_SETTABLE_DEVIATION_BPS, InvalidDeviationBps(priceDeviationToleranceBps_) + ); + priceDeviationToleranceBps = priceDeviationToleranceBps_; + emit PriceDeviationToleranceBpsSet(priceDeviationToleranceBps_); + } + /** * @inheritdoc IClaimManager */ @@ -286,6 +333,81 @@ contract ClaimManager is */ function _authorizeUpgrade(address) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + /** + * @notice Computes the Chainlink fair value of `amountIn` units of `tokenIn` denominated in `tokenOut`. + * @dev Normalizes the NATIVE_ETH sentinel to the Swapper's nativeWrapper before feed lookups, mirroring + * Swapper.quoteSwap so a slashed-native-ETH leg priced against a WETH feed works correctly. + * Both external Chainlink calls are wrapped in try/catch so staleness reverts surface as + * OracleUnavailable rather than propagating raw Chainlink errors. + * @param tokenIn Collateral token (may be the NATIVE_ETH sentinel). + * @param tokenOut Payout token (may be the NATIVE_ETH sentinel). + * @param amountIn Amount of tokenIn to value. + * @return fair Token-native amount of tokenOut equivalent to amountIn of tokenIn at Chainlink prices. + */ + function _chainlinkFairValue( + address tokenIn, + address tokenOut, + uint256 amountIn + ) + private + view + returns (uint256 fair) + { + address feed = chainlinkPriceFeed; + if (feed == address(0)) revert OracleUnavailable(tokenIn, tokenOut); + + address actualIn = tokenIn == _NATIVE_ETH ? ISwapper(swapper).nativeWrapper() : tokenIn; + address actualOut = tokenOut == _NATIVE_ETH ? ISwapper(swapper).nativeWrapper() : tokenOut; + + if (actualIn == actualOut) return amountIn; + + IChainlinkPriceFeed oracle = IChainlinkPriceFeed(feed); + + if (!oracle.hasPriceFeed(actualIn) || !oracle.hasPriceFeed(actualOut)) { + revert OracleUnavailable(tokenIn, tokenOut); + } + + uint256 usd; + try oracle.getUSDValue(actualIn, amountIn) returns (uint256 v) { + usd = v; + } catch { + revert OracleUnavailable(tokenIn, tokenOut); + } + if (usd == 0) revert OracleUnavailable(tokenIn, tokenOut); + + try oracle.getTokenAmount(actualOut, usd) returns (uint256 v) { + fair = v; + } catch { + revert OracleUnavailable(tokenIn, tokenOut); + } + if (fair == 0) revert OracleUnavailable(tokenIn, tokenOut); + } + + /** + * @notice Validates that a DEX spot quote is within the configured tolerance of the Chainlink fair value. + * @dev Reverts with QuoteDeviatesFromOracle when the absolute deviation exceeds priceDeviationToleranceBps. + * Callers must guarantee chainlinkFair > 0 (enforced by _chainlinkFairValue). + * @param tokenIn Collateral token (used only in the revert payload). + * @param tokenOut Payout token (used only in the revert payload). + * @param dexQuote Spot quote returned by the DEX route. + * @param chainlinkFair Chainlink-derived fair value for the same amount. + */ + function _validateDexAgainstOracle( + address tokenIn, + address tokenOut, + uint256 dexQuote, + uint256 chainlinkFair + ) + private + view + { + uint256 diff = dexQuote > chainlinkFair ? dexQuote - chainlinkFair : chainlinkFair - dexQuote; + uint256 deviationBps = (diff * uint256(_MAX_SLIPPAGE_BPS)) / chainlinkFair; + if (deviationBps > priceDeviationToleranceBps) { + revert QuoteDeviatesFromOracle(tokenIn, tokenOut, dexQuote, chainlinkFair, deviationBps); + } + } + /** * @notice Files a claim record and returns policy metadata. * @param policyId Identifier of the policy to claim against. @@ -479,7 +601,9 @@ contract ClaimManager is /** * @notice Computes per-vault token-native slash amounts proportional to each vault's payout-equivalent value. - * @dev Two-pass: quotes each stake to payout-token equivalent, then computes proportional slash amounts. + * @dev Two-pass: values each stake via Chainlink fair value (oracle-anchored), then computes proportional + * slash amounts. The DEX quote is used as a bounded sanity check — if it deviates from the Chainlink + * fair value by more than `priceDeviationToleranceBps`, the whole claim reverts. * Cross-token slashes are inflated by MAX / (MAX - maxSwapSlippageBps) so the beneficiary receives * the full requestedAmount even at worst-case swap slippage; same-token vaults need no inflation. * @param vaults Vault addresses from previewSlashing. @@ -512,14 +636,24 @@ contract ClaimManager is if (tokens[i] == payoutToken) { payoutEquivalents[i] = tokenStakes[i]; } else { + // Chainlink fair value is authoritative for slash distribution sizing. + // The DEX quote is a bounded sanity check: revert if it deviates too far from fair value, + // indicating a manipulated or illiquid pool. // slither-disable-next-line calls-loop - try ISwapper(swapper).quoteSwap(tokens[i], payoutToken, tokenStakes[i]) returns (uint256 quoted) { - payoutEquivalents[i] = quoted; - } catch {} + uint256 fair = _chainlinkFairValue(tokens[i], payoutToken, tokenStakes[i]); - if (payoutEquivalents[i] == 0) { - emit VaultExcludedFromSlashing(vaults[i], tokens[i]); + uint256 dex; + // slither-disable-next-line calls-loop + try ISwapper(swapper).quoteSwap(tokens[i], payoutToken, tokenStakes[i]) returns (uint256 quoted) { + dex = quoted; + } catch { + revert QuoteUnavailable(tokens[i], payoutToken); } + if (dex == 0) revert QuoteUnavailable(tokens[i], payoutToken); + + _validateDexAgainstOracle(tokens[i], payoutToken, dex, fair); + + payoutEquivalents[i] = fair; } totalPayoutEquivalent += payoutEquivalents[i]; } @@ -565,8 +699,9 @@ contract ClaimManager is /** * @notice Processes collateral by swapping or transferring directly, capped at remainingPayout. - * @dev Uses quoteSwap for amountOutMin instead of oracle-based pricing. - * Surplus beyond remainingPayout is forwarded via PremiumManager.distributeSurplusAsRewards() if configured. + * @dev amountOutMin is derived from Chainlink fair value with the DEX quote as a bounded sanity check + * (see `_computeAmountOutMin`). Surplus beyond remainingPayout is forwarded via + * PremiumManager.distributeSurplusAsRewards() if configured. * vaultIndex is included in the taskId to prevent replay collisions when two vault legs * share the same collateral token. * @param policyId Identifier of the policy. @@ -643,8 +778,18 @@ contract ClaimManager is } /** - * @notice Quotes a swap and applies the configured slippage buffer to derive the minimum acceptable output. - * @dev Reverts with QuoteUnavailable if the swapper returns zero or reverts. + * @notice Derives the minimum acceptable swap output, anchored to Chainlink fair value with a bounded + * DEX sanity check. + * @dev The Chainlink fair value is the primary anchor; the DEX quote is validated against it via + * `_validateDexAgainstOracle` and then used to determine the tighter of the two bounds: + * anchor = min(fair, dex) + * amountOutMin = anchor × (MAX − maxSwapSlippageBps) / MAX + * Using min(fair, dex) ensures the bound is achievable on-chain when the DEX quote is within + * tolerance below the Chainlink fair value, while a manipulated DEX quote above fair is capped + * to fair (preventing an inflated bound that cannot be met). + * Reverts with OracleUnavailable if the Chainlink feed is unset, missing, stale, or returns zero. + * Reverts with QuoteUnavailable if the DEX quote is unavailable or zero. + * Reverts with QuoteDeviatesFromOracle if the DEX quote deviates beyond priceDeviationToleranceBps. * Returns at least 1 to prevent a zero-minimum swap that would accept any output. * @param tokenIn Collateral token to sell. * @param tokenOut Payout token to receive. @@ -652,13 +797,21 @@ contract ClaimManager is * @return Slippage-adjusted minimum output amount (≥ 1). */ function _computeAmountOutMin(address tokenIn, address tokenOut, uint256 amountIn) private returns (uint256) { + uint256 fair = _chainlinkFairValue(tokenIn, tokenOut, amountIn); + + uint256 dex; try ISwapper(swapper).quoteSwap(tokenIn, tokenOut, amountIn) returns (uint256 quoted) { - if (quoted == 0) revert QuoteUnavailable(tokenIn, tokenOut); - uint256 amountOutMin = (quoted * (uint256(_MAX_SLIPPAGE_BPS) - maxSwapSlippageBps)) / _MAX_SLIPPAGE_BPS; - return amountOutMin == 0 ? 1 : amountOutMin; + dex = quoted; } catch { revert QuoteUnavailable(tokenIn, tokenOut); } + if (dex == 0) revert QuoteUnavailable(tokenIn, tokenOut); + + _validateDexAgainstOracle(tokenIn, tokenOut, dex, fair); + + uint256 anchor = fair < dex ? fair : dex; + uint256 amountOutMin = (anchor * (uint256(_MAX_SLIPPAGE_BPS) - maxSwapSlippageBps)) / _MAX_SLIPPAGE_BPS; + return amountOutMin == 0 ? 1 : amountOutMin; } // slither-disable-end calls-loop diff --git a/src/interfaces/IChainlinkPriceFeed.sol b/src/interfaces/IChainlinkPriceFeed.sol index 031af7f8..cb36e915 100644 --- a/src/interfaces/IChainlinkPriceFeed.sol +++ b/src/interfaces/IChainlinkPriceFeed.sol @@ -13,4 +13,19 @@ interface IChainlinkPriceFeed { * @return USD value with 8 decimals. */ function getUSDValue(address token, uint256 amount) external view returns (uint256); + + /** + * @notice Returns the token amount corresponding to a USD value. + * @param token ERC20 token address. + * @param amountUSD USD value with 8 decimals (Chainlink standard). + * @return Token amount in the token's native decimals. + */ + function getTokenAmount(address token, uint256 amountUSD) external view returns (uint256); + + /** + * @notice Returns whether a Chainlink price feed is registered for the given token. + * @param token ERC20 token address. + * @return True if a price feed exists for the token. + */ + function hasPriceFeed(address token) external view returns (bool); } diff --git a/src/interfaces/IClaimManager.sol b/src/interfaces/IClaimManager.sol index abeddbcc..153aedcd 100644 --- a/src/interfaces/IClaimManager.sol +++ b/src/interfaces/IClaimManager.sol @@ -149,6 +149,18 @@ interface IClaimManager is IAccessControl { */ event PremiumManagerSet(address indexed premiumManager); + /** + * @notice Emitted when the Chainlink price feed address is updated. + * @param chainlinkPriceFeed New ChainlinkPriceFeed address. + */ + event ChainlinkPriceFeedSet(address indexed chainlinkPriceFeed); + + /** + * @notice Emitted when the price deviation tolerance is updated. + * @param priceDeviationToleranceBps New tolerance in basis points. + */ + event PriceDeviationToleranceBpsSet(uint16 priceDeviationToleranceBps); + /** * @notice Emitted when surplus collateral or payout tokens are distributed as restaker rewards. * @dev Surplus arises in two cases: (1) cross-token path — swap output exceeds remainingPayout; @@ -195,14 +207,14 @@ interface IClaimManager is IAccessControl { event TokensRecovered(address indexed token, address indexed to, uint256 amount); /** - * @notice Emitted when a cross-token vault is excluded from slashing. This occurs when either: - * (a) `quoteSwap` reverts or returns zero (no functioning swap route), or (b) the - * proportional slash amount rounds to zero after slippage inflation. - * @dev Partial exclusion is not reverted; remaining vaults absorb the full claim pro-rata. - * Monitor this event to detect misconfigured swap routes before a loss event. + * @notice Emitted when a cross-token vault is excluded from slashing because the Chainlink-anchored + * proportional slash amount rounds to zero after slippage inflation (dust vault). + * @dev Fires only from the proportional-rounding branch — not from an unavailable quote or oracle + * (those now revert with `QuoteUnavailable` or `OracleUnavailable` respectively). * Same-token vaults are never excluded — a zero proportional slash is forced to 1 instead. + * Monitor this event to detect dust vaults contributing negligible collateral to a claim. * @param vault Address of the excluded vault. - * @param token Collateral token for which the quote failed or slash rounded to zero. + * @param token Collateral token whose proportional slash rounded to zero. */ event VaultExcludedFromSlashing(address indexed vault, address indexed token); @@ -331,6 +343,34 @@ interface IClaimManager is IAccessControl { */ error QuoteUnavailable(address collateralToken, address payoutToken); + /** + * @notice Reverts when a Chainlink fair value cannot be computed for a cross-token collateral leg. + * This occurs when: the ClaimManager's chainlinkPriceFeed is unset, a feed is missing for + * one of the tokens, the feed data is stale, or the USD/token conversion yields zero. + * @param tokenIn Collateral token address (or NATIVE_ETH sentinel). + * @param tokenOut Payout token address (or NATIVE_ETH sentinel). + */ + error OracleUnavailable(address tokenIn, address tokenOut); + + /** + * @notice Reverts when the DEX spot quote deviates from the Chainlink fair value by more than + * the configured `priceDeviationToleranceBps`, indicating a manipulated or thin pool. + * @param tokenIn Collateral token address. + * @param tokenOut Payout token address. + * @param dexQuote Spot quote returned by the DEX route. + * @param chainlinkFair Chainlink-derived fair value for the same amount. + * @param deviationBps Actual deviation in basis points. + */ + error QuoteDeviatesFromOracle( + address tokenIn, address tokenOut, uint256 dexQuote, uint256 chainlinkFair, uint256 deviationBps + ); + + /** + * @notice Reverts when the requested `priceDeviationToleranceBps` exceeds the maximum settable bound. + * @param bps The invalid value provided. + */ + error InvalidDeviationBps(uint16 bps); + /** * @notice Reverts when previewSlashing returns arrays of different lengths. * @param vaultsLength Length of the vaults array. @@ -507,4 +547,39 @@ interface IClaimManager is IAccessControl { * @param amount Amount to transfer. */ function recoverTokens(address token, address to, uint256 amount) external; + + /** + * @notice Initializes V2 state atomically during a UUPS upgrade via upgradeToAndCall. + * @dev Uses OpenZeppelin's `reinitializer(2)` pattern. Must be called exactly once, embedded in the + * upgradeToAndCall calldata. For fresh deployments, call immediately after `initialize`. + * @param chainlinkPriceFeed_ Address of the ChainlinkPriceFeed contract. + * @param priceDeviationToleranceBps_ Maximum allowed deviation between DEX quote and Chainlink fair + * value, in basis points. Must be <= 1000 (10%). + */ + function initializeV2(address chainlinkPriceFeed_, uint16 priceDeviationToleranceBps_) external; + + /** + * @notice Sets the Chainlink price feed address used for cross-token claim valuation. + * @dev Requires DEFAULT_ADMIN_ROLE. + * @param chainlinkPriceFeed_ Address of the ChainlinkPriceFeed contract. Must be non-zero. + */ + function setChainlinkPriceFeed(address chainlinkPriceFeed_) external; + + /** + * @notice Sets the maximum allowed deviation between DEX quote and Chainlink fair value. + * @dev Requires DEFAULT_ADMIN_ROLE. Must be <= 1000 bps (10%). + * @param priceDeviationToleranceBps_ Tolerance in basis points (e.g. 300 = 3%). + */ + function setPriceDeviationToleranceBps(uint16 priceDeviationToleranceBps_) external; + + /** + * @notice Returns the Chainlink price feed address used for cross-token claim valuation. + */ + function chainlinkPriceFeed() external view returns (address); + + /** + * @notice Returns the maximum allowed deviation between DEX spot quote and Chainlink fair value, + * in basis points. Cross-token claims revert with `QuoteDeviatesFromOracle` if exceeded. + */ + function priceDeviationToleranceBps() external view returns (uint16); } diff --git a/test/defi/mocks/CoverageMocks.sol b/test/defi/mocks/CoverageMocks.sol index 98c1f9d3..6f7bd17b 100644 --- a/test/defi/mocks/CoverageMocks.sol +++ b/test/defi/mocks/CoverageMocks.sol @@ -69,6 +69,14 @@ contract MockChainlinkPriceFeedDefi is IChainlinkPriceFeed { function getUSDValue(address, uint256 amount) external pure override returns (uint256) { return amount; } + + function getTokenAmount(address, uint256 amountUSD) external pure override returns (uint256) { + return amountUSD; + } + + function hasPriceFeed(address) external pure override returns (bool) { + return true; + } } contract MockCoverPoolFactory { diff --git a/test/fuzz/ClaimManager.t.sol b/test/fuzz/ClaimManager.t.sol index 671180b9..ab8d82de 100644 --- a/test/fuzz/ClaimManager.t.sol +++ b/test/fuzz/ClaimManager.t.sol @@ -101,6 +101,7 @@ contract FuzzSlashingManager { contract FuzzSwapper { uint256 private _output; address private _outToken; + address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); function configure(address outToken, uint256 output) external { _outToken = outToken; @@ -121,6 +122,21 @@ contract FuzzSwapper { } } +/// @dev 1:1 Chainlink feed mock for fuzz tests. All tokens have a feed; getUSDValue and getTokenAmount are 1:1. +contract FuzzChainlinkPriceFeed { + function hasPriceFeed(address) external pure returns (bool) { + return true; + } + + function getUSDValue(address, uint256 amount) external pure returns (uint256) { + return amount; + } + + function getTokenAmount(address, uint256 usdValue) external pure returns (uint256) { + return usdValue; + } +} + contract FuzzPolicy { mapping(uint256 => IPolicyManager.PolicyMetadata) private _metadata; @@ -159,6 +175,7 @@ contract ClaimManagerFuzzTest is Test { FuzzCoverPool internal pool; FuzzToken internal collateralToken; FuzzToken internal payoutToken; + FuzzChainlinkPriceFeed internal chainlinkFeed; address internal admin = makeAddr("admin"); address internal claimer = makeAddr("claimer"); @@ -178,6 +195,7 @@ contract ClaimManagerFuzzTest is Test { pool = new FuzzCoverPool(operator); collateralToken = new FuzzToken("Collateral", "COL"); payoutToken = new FuzzToken("Payout", "PAY"); + chainlinkFeed = new FuzzChainlinkPriceFeed(); ClaimManager impl = new ClaimManager(); address proxy = address( @@ -190,6 +208,7 @@ contract ClaimManagerFuzzTest is Test { ) ); claimManager = ClaimManager(payable(proxy)); + claimManager.initializeV2(address(chainlinkFeed), 300); vm.warp(START); _setupPolicy(COVERAGE_LIMIT()); diff --git a/test/unit/ClaimManager.t.sol b/test/unit/ClaimManager.t.sol index 6167c51c..ff90d6e6 100644 --- a/test/unit/ClaimManager.t.sol +++ b/test/unit/ClaimManager.t.sol @@ -47,6 +47,7 @@ contract ClaimManagerTest is Test { MockToken internal collateralTokenMock; MockToken internal payoutTokenMock; MockCoverPool internal mockPool; + MockChainlinkPriceFeed internal chainlinkFeedMock; bytes32 internal evidenceHash; bytes32 internal policyCommit; @@ -84,6 +85,8 @@ contract ClaimManagerTest is Test { event TokensRecovered(address indexed token, address indexed to, uint256 amount); event VaultExcludedFromSlashing(address indexed vault, address indexed token); event PremiumManagerSet(address indexed premiumManager); + event ChainlinkPriceFeedSet(address indexed chainlinkPriceFeed); + event PriceDeviationToleranceBpsSet(uint16 priceDeviationToleranceBps); event SwapSurplusDistributed( uint96 indexed policyId, uint256 indexed claimId, address indexed payoutToken, uint256 surplusAmount ); @@ -141,6 +144,8 @@ contract ClaimManagerTest is Test { vm.label(collateralToken, "CollateralToken"); vm.label(payoutToken, "PayoutToken"); + chainlinkFeedMock = new MockChainlinkPriceFeed(); + Options memory deployOpts; deployOpts.unsafeSkipAllChecks = true; address proxy = Upgrades.deployUUPSProxy( @@ -150,7 +155,11 @@ contract ClaimManagerTest is Test { ); claimManager = ClaimManager(payable(proxy)); + vm.prank(admin); + claimManager.initializeV2(address(chainlinkFeedMock), 300); + vm.label(address(claimManager), "ClaimManager"); + vm.label(address(chainlinkFeedMock), "ChainlinkPriceFeed"); _setupDefaultPolicy(); } @@ -211,7 +220,117 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - PAUSE / UNPAUSE + INITIALIZE V2 + //////////////////////////////////////////////////////////////*/ + + function test_initializeV2_WhenZeroChainlink_Reverts() public { + ClaimManager impl = new ClaimManager(); + ClaimManager cm = ClaimManager(payable(address(new ERC1967Proxy(address(impl), "")))); + cm.initialize(admin, specRegistry, slashingManager, swapper, policy); + vm.expectRevert(IClaimManager.ZeroAddress.selector); + cm.initializeV2(address(0), 300); + } + + function test_initializeV2_WhenAboveDeviationBound_Reverts() public { + ClaimManager impl = new ClaimManager(); + ClaimManager cm = ClaimManager(payable(address(new ERC1967Proxy(address(impl), "")))); + cm.initialize(admin, specRegistry, slashingManager, swapper, policy); + vm.expectRevert(abi.encodeWithSelector(IClaimManager.InvalidDeviationBps.selector, uint16(1001))); + cm.initializeV2(address(chainlinkFeedMock), 1001); + } + + function test_initializeV2_WhenCalledTwice_Reverts() public { + vm.expectRevert(); + claimManager.initializeV2(address(chainlinkFeedMock), 300); + } + + function test_initializeV2_EmitsBothSetterEvents() public { + ClaimManager impl = new ClaimManager(); + ClaimManager cm = ClaimManager(payable(address(new ERC1967Proxy(address(impl), "")))); + cm.initialize(admin, specRegistry, slashingManager, swapper, policy); + + vm.expectEmit(true, false, false, false, address(cm)); + emit ChainlinkPriceFeedSet(address(chainlinkFeedMock)); + vm.expectEmit(false, false, false, true, address(cm)); + emit PriceDeviationToleranceBpsSet(300); + + cm.initializeV2(address(chainlinkFeedMock), 300); + } + + function test_initializeV2_PopulatesStateCorrectly() public view { + assertEq(claimManager.chainlinkPriceFeed(), address(chainlinkFeedMock)); + assertEq(claimManager.priceDeviationToleranceBps(), 300); + } + + /*////////////////////////////////////////////////////////////// + SET CHAINLINK PRICE FEED + //////////////////////////////////////////////////////////////*/ + + function test_setChainlinkPriceFeed_WhenCallerNotAdmin_Reverts() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + unauthorized, + claimManager.DEFAULT_ADMIN_ROLE() + ) + ); + vm.prank(unauthorized); + claimManager.setChainlinkPriceFeed(address(chainlinkFeedMock)); + } + + function test_setChainlinkPriceFeed_WhenZero_Reverts() public { + vm.expectRevert(IClaimManager.ZeroAddress.selector); + vm.prank(admin); + claimManager.setChainlinkPriceFeed(address(0)); + } + + function test_setChainlinkPriceFeed_EmitsEvent() public { + address newFeed = makeAddr("newFeed"); + vm.expectEmit(true, false, false, false, address(claimManager)); + emit ChainlinkPriceFeedSet(newFeed); + vm.prank(admin); + claimManager.setChainlinkPriceFeed(newFeed); + assertEq(claimManager.chainlinkPriceFeed(), newFeed); + } + + /*////////////////////////////////////////////////////////////// + SET PRICE DEVIATION TOLERANCE BPS + //////////////////////////////////////////////////////////////*/ + + function test_setPriceDeviationToleranceBps_WhenCallerNotAdmin_Reverts() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + unauthorized, + claimManager.DEFAULT_ADMIN_ROLE() + ) + ); + vm.prank(unauthorized); + claimManager.setPriceDeviationToleranceBps(300); + } + + function test_setPriceDeviationToleranceBps_WhenAboveBound_Reverts() public { + vm.expectRevert(abi.encodeWithSelector(IClaimManager.InvalidDeviationBps.selector, uint16(1001))); + vm.prank(admin); + claimManager.setPriceDeviationToleranceBps(1001); + } + + function test_setPriceDeviationToleranceBps_WhenAtBound_Succeeds() public { + vm.prank(admin); + claimManager.setPriceDeviationToleranceBps(1000); + assertEq(claimManager.priceDeviationToleranceBps(), 1000); + } + + function test_setPriceDeviationToleranceBps_EmitsEvent() public { + vm.expectEmit(false, false, false, true, address(claimManager)); + emit PriceDeviationToleranceBpsSet(500); + vm.prank(admin); + claimManager.setPriceDeviationToleranceBps(500); + assertEq(claimManager.priceDeviationToleranceBps(), 500); + } + + /*////////////////////////////////////////////////////////////// + PAUSE / UNPAUSE //////////////////////////////////////////////////////////////*/ function test_pause_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( @@ -688,6 +807,287 @@ contract ClaimManagerTest is Test { claimManager.recoverTokens(collateralToken, beneficiary, 0); } + /*////////////////////////////////////////////////////////////// + FILE CLAIM — ORACLE PROTECTION (CYS3-03) + //////////////////////////////////////////////////////////////*/ + + function test_fileClaim_WhenChainlinkPriceFeedUnset_RevertsOracleUnavailable() public { + // Deploy a fresh ClaimManager that was never given initializeV2 (chainlinkPriceFeed == address(0)) + Options memory opts; + opts.unsafeSkipAllChecks = true; + address freshProxy = Upgrades.deployUUPSProxy( + "ClaimManager.sol", + abi.encodeCall(ClaimManager.initialize, (admin, specRegistry, slashingManager, swapper, policy)), + opts + ); + ClaimManager freshCm = ClaimManager(payable(freshProxy)); + + // Wire policy to fresh proxy + policyMock.setPolicyMetadata(POLICY_ID, _defaultMetadata()); + + // collateralToken != payoutToken → cross-token path → OracleUnavailable (feed address is address(0)) + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert(abi.encodeWithSelector(IClaimManager.OracleUnavailable.selector, collateralToken, payoutToken)); + vm.prank(claimer); + freshCm.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + + function test_fileClaim_WhenChainlinkFeedMissingForCollateral_RevertsOracleUnavailable() public { + chainlinkFeedMock.setFeedMissing(collateralToken); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert(abi.encodeWithSelector(IClaimManager.OracleUnavailable.selector, collateralToken, payoutToken)); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + + function test_fileClaim_WhenChainlinkFeedMissingForPayoutToken_RevertsOracleUnavailable() public { + chainlinkFeedMock.setFeedMissing(payoutToken); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert(abi.encodeWithSelector(IClaimManager.OracleUnavailable.selector, collateralToken, payoutToken)); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + + function test_fileClaim_WhenChainlinkPriceStale_RevertsOracleUnavailable() public { + chainlinkFeedMock.setShouldRevert(true); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert(abi.encodeWithSelector(IClaimManager.OracleUnavailable.selector, collateralToken, payoutToken)); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + + function test_fileClaim_WhenDexQuoteDeviatesAboveTolerance_RevertsQuoteDeviatesFromOracle() public { + // Configure a swapper where quoteSwap for collateralToken returns 5000 ether + // but Chainlink fair value is 1:1 = SLASH_AMOUNT. + // Deviation = (5000 ether - 500 ether) / 500 ether * 10000 = 90000 bps >> 300 bps tolerance. + MockMultiSwapper deviatingSwapper = new MockMultiSwapper(); + deviatingSwapper.setQuoteOutput(collateralToken, 5000 ether); // quoteSwap returns 5000 ether + deviatingSwapper.setSwapOutput(collateralToken, 5000 ether); + deviatingSwapper.setPayoutToken(payoutToken); + vm.prank(admin); + claimManager.setSwapper(address(deviatingSwapper)); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + // chainlinkFair = SLASH_AMOUNT (1:1); dex = 5000 ether → huge upward deviation + vm.expectRevert( + abi.encodeWithSelector( + IClaimManager.QuoteDeviatesFromOracle.selector, + collateralToken, + payoutToken, + uint256(5000 ether), + uint256(SLASH_AMOUNT), + uint256(90_000) + ) + ); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + + function test_fileClaim_WhenDexQuoteDeviatesBelowTolerance_RevertsQuoteDeviatesFromOracle() public { + // Configure a swapper where quoteSwap for collateralToken returns 400 ether (20% below 500) + // but Chainlink fair value is 1:1 = SLASH_AMOUNT = 500 ether. + // Deviation = (500 - 400) / 500 * 10000 = 2000 bps >> 300 bps tolerance. + MockMultiSwapper deviatingSwapper = new MockMultiSwapper(); + deviatingSwapper.setQuoteOutput(collateralToken, 400 ether); // quoteSwap returns 400 ether + deviatingSwapper.setSwapOutput(collateralToken, 400 ether); + deviatingSwapper.setPayoutToken(payoutToken); + vm.prank(admin); + claimManager.setSwapper(address(deviatingSwapper)); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert( + abi.encodeWithSelector( + IClaimManager.QuoteDeviatesFromOracle.selector, + collateralToken, + payoutToken, + uint256(400 ether), + uint256(SLASH_AMOUNT), + uint256(2000) + ) + ); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + + function test_fileClaim_WhenDexQuoteWithinTolerance_SlashSizingUsesChainlinkFair() public { + // DEX quote and Chainlink fair are both 1:1 (deviation = 0 bps → well within 300 bps tolerance). + // Slash sizing uses payoutEquivalents[i] = _chainlinkFairValue(token, payout, stake) = stake. + // The proportional slash is: baseSlash = stake * requested / stake = requested, inflated by slippage. + uint256 stakeA = 300 ether; + uint256 requestedAmount = 100 ether; + + // Default quoteSwap is 1:1; executeSwap mints requestedAmount to beneficiary. + MockMultiSwapper multiSwap = new MockMultiSwapper(); + multiSwap.setSwapOutput(collateralToken, requestedAmount); + multiSwap.setPayoutToken(payoutToken); + vm.prank(admin); + claimManager.setSwapper(address(multiSwap)); + + // Single cross-token vault: stakeA of collateralToken + MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); + address[] memory pvaults = new address[](1); + pvaults[0] = makeAddr("vA"); + address[] memory ptokens = new address[](1); + ptokens[0] = collateralToken; + uint256[] memory pstakes = new uint256[](1); + pstakes[0] = stakeA; + slashMock.setupPreview(pvaults, ptokens, pstakes); + // ~102 ether of collateral will be slashed (100 requested inflated by 2% slippage buffer) + collateralTokenMock.mint(address(slashMock), stakeA); + vm.prank(admin); + claimManager.setSlashingManager(address(slashMock)); + + specMock.setEvaluationResult(true, keccak256("approved")); + + uint256 balanceBefore = payoutTokenMock.balanceOf(beneficiary); + vm.prank(claimer); + uint256 claimId = claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); + + assertEq(uint8(claimManager.claimRecord(POLICY_ID, claimId).status), uint8(IClaimManager.ClaimStatus.Approved)); + // Beneficiary receives up to requestedAmount (excess is surplus-distributed or stranded) + assertGt(payoutTokenMock.balanceOf(beneficiary) - balanceBefore, 0, "Beneficiary should receive payout"); + } + + function test_fileClaim_WhenDexBelowFairWithinTolerance_AmountOutMinAnchoredToDex() public { + // DEX 1.5% below fair → anchor = dex; amountOutMin = dex * (1 - slippage) + // The swap should succeed because amountOutMin is achievable. + uint256 dexQuote = (SLASH_AMOUNT * 9850) / 10_000; // 1.5% below + + MockMultiSwapper nearFairSwapper = new MockMultiSwapper(); + nearFairSwapper.setQuoteOutput(collateralToken, dexQuote); // quoteSwap returns dexQuote + nearFairSwapper.setSwapOutput(collateralToken, dexQuote); // executeSwap also returns dexQuote + nearFairSwapper.setPayoutToken(payoutToken); + vm.prank(admin); + claimManager.setSwapper(address(nearFairSwapper)); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + collateralTokenMock.mint(address(slashingManagerMock), SLASH_AMOUNT); + slashingManagerMock.setTokenToTransfer(collateralToken); + + vm.prank(claimer); + uint256 claimId = claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + assertEq(uint8(claimManager.claimRecord(POLICY_ID, claimId).status), uint8(IClaimManager.ClaimStatus.Approved)); + } + + function test_fileClaim_WhenDexAboveFairWithinTolerance_AmountOutMinAnchoredToFair() public { + // DEX 1.5% above fair → anchor = fair; amountOutMin = fair * (1 - slippage) + // The swap should succeed because the swap produces at least the fair-anchored minimum. + uint256 dexQuote = (SLASH_AMOUNT * 10_150) / 10_000; // 1.5% above + + MockMultiSwapper aboveFairSwapper = new MockMultiSwapper(); + aboveFairSwapper.setQuoteOutput(collateralToken, dexQuote); // quoteSwap returns above-fair + aboveFairSwapper.setSwapOutput(collateralToken, dexQuote); // executeSwap produces dexQuote + aboveFairSwapper.setPayoutToken(payoutToken); + vm.prank(admin); + claimManager.setSwapper(address(aboveFairSwapper)); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + collateralTokenMock.mint(address(slashingManagerMock), SLASH_AMOUNT); + slashingManagerMock.setTokenToTransfer(collateralToken); + + vm.prank(claimer); + uint256 claimId = claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + assertEq(uint8(claimManager.claimRecord(POLICY_ID, claimId).status), uint8(IClaimManager.ClaimStatus.Approved)); + } + + function test_fileClaim_NativeEthCollateral_NormalizesToWrapperForChainlink() public { + // When the collateral is NATIVE_ETH, _chainlinkFairValue must query nativeWrapper not NATIVE_ETH. + // Mark NATIVE_ETH as missing but nativeWrapper as present in the mock feed. + address nativeWrapper = swapperMock.nativeWrapper(); + chainlinkFeedMock.setFeedMissing(NATIVE_ETH); // ensures we only pass via nativeWrapper path + + // Set up a policy where payoutToken == nativeWrapper so same-token path is taken for NATIVE_ETH + // (after normalization, NATIVE_ETH → nativeWrapper == payoutToken → same-token path → no oracle needed) + IPolicyManager.PolicyMetadata memory meta = _defaultMetadata(); + meta.payoutToken = nativeWrapper; + meta.claimer = claimer; + policyMock.setPolicyMetadata(POLICY_ID, meta); + + specMock.setEvaluationResult(true, keccak256("approved")); + + MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); + address[] memory pvaults = new address[](1); + pvaults[0] = makeAddr("ethVault"); + address[] memory ptokens = new address[](1); + ptokens[0] = NATIVE_ETH; + uint256[] memory pstakes = new uint256[](1); + pstakes[0] = SLASH_AMOUNT; + slashMock.setupPreview(pvaults, ptokens, pstakes); + vm.deal(address(slashMock), SLASH_AMOUNT); + vm.prank(admin); + claimManager.setSlashingManager(address(slashMock)); + + // payoutToken == nativeWrapper; NATIVE_ETH normalizes to nativeWrapper → same-token path → no oracle needed + // slashMock sends ETH; claimManager receives it and transfers to beneficiary (native ETH payout) + // This test verifies that NATIVE_ETH → nativeWrapper normalization does NOT trigger OracleUnavailable + // even when the NATIVE_ETH sentinel's feed is marked missing. + vm.prank(claimer); + // Expected: no OracleUnavailable revert (NATIVE_ETH → nativeWrapper → same-token path) + // May revert for other reasons (native transfer), so just assert no oracle error: + try claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, "") { + // success is fine + } + catch (bytes memory data) { + bytes4 oracleSelector = IClaimManager.OracleUnavailable.selector; + if (data.length >= 4) { + bytes4 gotSelector; + assembly { + gotSelector := mload(add(data, 0x20)) + } + assertFalse(gotSelector == oracleSelector, "Should not revert OracleUnavailable for NATIVE_ETH"); + } + } + } + /*////////////////////////////////////////////////////////////// FILE CLAIM //////////////////////////////////////////////////////////////*/ @@ -1118,6 +1518,7 @@ contract ClaimManagerTest is Test { MockMultiSwapper multiSwapper = new MockMultiSwapper(); multiSwapper.setSwapOutput(collateralToken, firstSwapOutput); multiSwapper.setSwapOutput(collateralToken2, secondSwapOutput); + // quoteSwap defaults to 1:1 (amountIn) — oracle deviation gate passes automatically. multiSwapper.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(multiSwapper)); @@ -1136,7 +1537,7 @@ contract ClaimManagerTest is Test { assertEq(uint8(record.status), uint8(IClaimManager.ClaimStatus.Approved)); } - function test_fileClaim_WhenQuoteSwapFailsForOneVault_EmitsVaultExcludedFromSlashing() public { + function test_fileClaim_WhenQuoteSwapReverts_RevertsQuoteUnavailable() public { uint256 requestedAmount = 100 ether; uint256 quotableStake = 300 ether; uint256 unquotableStake = 200 ether; @@ -1171,14 +1572,12 @@ contract ClaimManagerTest is Test { specMock.setEvaluationResult(true, keccak256("approved")); - vm.expectEmit(true, true, false, false, address(claimManager)); - emit VaultExcludedFromSlashing(vault2, unquotableToken); - + vm.expectRevert(abi.encodeWithSelector(IClaimManager.QuoteUnavailable.selector, unquotableToken, payoutToken)); vm.prank(claimer); claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); } - function test_fileClaim_WhenQuoteSwapReturnsZero_EmitsVaultExcludedFromSlashing() public { + function test_fileClaim_WhenQuoteSwapReturnsZero_RevertsQuoteUnavailable() public { uint256 requestedAmount = 100 ether; uint256 quotableStake = 300 ether; uint256 zeroQuoteStake = 200 ether; @@ -1213,26 +1612,42 @@ contract ClaimManagerTest is Test { specMock.setEvaluationResult(true, keccak256("approved")); - vm.expectEmit(true, true, false, false, address(claimManager)); - emit VaultExcludedFromSlashing(vault2, zeroQuoteToken); - + vm.expectRevert(abi.encodeWithSelector(IClaimManager.QuoteUnavailable.selector, zeroQuoteToken, payoutToken)); vm.prank(claimer); claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); } - function test_fileClaim_WhenQuoteSwapFailsForOneVault_RemainingVaultSlashedCorrectly() public { - _setupTwoVaultScenarioWithUnquotable(); + function test_fileClaim_WhenOneVaultUnquotable_RevertsQuoteUnavailable() public { + MockToken unquotableTokenMock = new MockToken("Unquotable2", "UNQ2"); + address unquotableToken = address(unquotableTokenMock); - uint256 beneficiaryBefore = payoutTokenMock.balanceOf(beneficiary); + MockSwapperWithUnquotable sw = new MockSwapperWithUnquotable(); + sw.setQuoteReverts(unquotableToken); + sw.setPayoutToken(payoutToken); + sw.setSwapOutput(100 ether); + vm.prank(admin); + claimManager.setSwapper(address(sw)); - vm.prank(claimer); - uint256 claimId = claimManager.fileClaim(POLICY_ID, 100 ether, evidenceHash, ""); + MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); + address[] memory pvaults = new address[](2); + pvaults[0] = makeAddr("vault1"); + pvaults[1] = makeAddr("vault2"); + address[] memory ptokens = new address[](2); + ptokens[0] = collateralToken; + ptokens[1] = unquotableToken; + uint256[] memory pstakes = new uint256[](2); + pstakes[0] = 300 ether; + pstakes[1] = 200 ether; + slashMock.setupPreview(pvaults, ptokens, pstakes); + collateralTokenMock.mint(address(slashMock), 300 ether); + vm.prank(admin); + claimManager.setSlashingManager(address(slashMock)); - uint256 received = payoutTokenMock.balanceOf(beneficiary) - beneficiaryBefore; - assertEq(received, 100 ether, "Remaining vault should cover full claim"); + specMock.setEvaluationResult(true, keccak256("approved")); - IClaimManager.ClaimRecord memory record = claimManager.claimRecord(POLICY_ID, claimId); - assertEq(uint8(record.status), uint8(IClaimManager.ClaimStatus.Approved)); + vm.expectRevert(abi.encodeWithSelector(IClaimManager.QuoteUnavailable.selector, unquotableToken, payoutToken)); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, 100 ether, evidenceHash, ""); } function _setupTwoVaultScenarioWithUnquotable() private { @@ -2219,6 +2634,7 @@ contract ClaimManagerTest is Test { MockNativeMultiSwapper nativeMultiSwapper = new MockNativeMultiSwapper(); nativeMultiSwapper.setSwapOutput(NATIVE_ETH, ethSwapOutput); nativeMultiSwapper.setSwapOutput(collateralToken, erc20SwapOutput); + // quoteSwap defaults to 1:1 (amountIn) — oracle deviation gate passes automatically. nativeMultiSwapper.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(nativeMultiSwapper)); @@ -2303,7 +2719,7 @@ contract ClaimManagerTest is Test { assertGt(actualPayout, 0); } - function test_fileClaim_WhenOneVaultUnquotable_SkipsUnquotableAndPaysFromQuotableVault() public { + function test_fileClaim_WhenOneVaultUnquotable_RevertsWithQuoteUnavailable() public { uint256 requestedAmount = 400 ether; uint256 quotableStake = 500 ether; uint256 unquotableStake = 500 ether; @@ -2335,26 +2751,12 @@ contract ClaimManagerTest is Test { specMock.setEvaluationResult(true, keccak256("approved")); - uint256 balanceBefore = payoutTokenMock.balanceOf(beneficiary); - + vm.expectRevert(abi.encodeWithSelector(IClaimManager.QuoteUnavailable.selector, unquotableToken, payoutToken)); vm.prank(claimer); - uint256 claimId = claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); - - IClaimManager.ClaimRecord memory record = claimManager.claimRecord(POLICY_ID, claimId); - assertEq(uint8(record.status), uint8(IClaimManager.ClaimStatus.Approved), "Claim should be approved"); - assertEq( - payoutTokenMock.balanceOf(beneficiary) - balanceBefore, - requestedAmount, - "Beneficiary should receive full requested payout from quotable vault" - ); - assertEq( - unquotableTokenMock.balanceOf(address(slashMock)), - 0, - "Unquotable token should not have been minted to slash mock" - ); + claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); } - function test_fileClaim_WhenVaultQuoteReturnsZero_SkipsVaultAndSucceeds() public { + function test_fileClaim_WhenVaultQuoteReturnsZero_RevertsQuoteUnavailable() public { uint256 requestedAmount = 300 ether; uint256 quotableStake = 400 ether; uint256 zeroQuoteStake = 400 ether; @@ -2386,17 +2788,12 @@ contract ClaimManagerTest is Test { specMock.setEvaluationResult(true, keccak256("approved")); - uint256 balanceBefore = payoutTokenMock.balanceOf(beneficiary); - + vm.expectRevert(abi.encodeWithSelector(IClaimManager.QuoteUnavailable.selector, zeroQuoteToken, payoutToken)); vm.prank(claimer); - uint256 claimId = claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); - - IClaimManager.ClaimRecord memory record = claimManager.claimRecord(POLICY_ID, claimId); - assertEq(uint8(record.status), uint8(IClaimManager.ClaimStatus.Approved), "Claim should be approved"); - assertGt(payoutTokenMock.balanceOf(beneficiary) - balanceBefore, 0, "Beneficiary should receive payout"); + claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); } - function test_fileClaim_WhenAllVaultsUnquotable_Reverts() public { + function test_fileClaim_WhenAllVaultsUnquotable_RevertsQuoteUnavailable() public { MockToken unquotableTokenMock = new MockToken("Unquotable2", "UNQ2"); address unquotableToken = address(unquotableTokenMock); @@ -2418,12 +2815,12 @@ contract ClaimManagerTest is Test { specMock.setEvaluationResult(true, keccak256("approved")); - vm.expectRevert(abi.encodeWithSelector(IClaimManager.NoCollateralSlashed.selector, POLICY_ID)); + vm.expectRevert(abi.encodeWithSelector(IClaimManager.QuoteUnavailable.selector, unquotableToken, payoutToken)); vm.prank(claimer); claimManager.fileClaim(POLICY_ID, COVERAGE_LIMIT, evidenceHash, ""); } - function test_fileClaim_WhenOneVaultUnquotable_ExecuteSlashingReceivesNoZeroAmountEntries() public { + function test_fileClaim_WhenOneVaultUnquotableDex_RevertsBeforeExecuteSlashing() public { uint256 requestedAmount = 300 ether; uint256 quotableStake = 400 ether; @@ -2454,13 +2851,11 @@ contract ClaimManagerTest is Test { specMock.setEvaluationResult(true, keccak256("approved")); + // Under the new oracle model, a missing DEX quote reverts the whole claim rather than + // silently excluding the vault from slashing distribution. + vm.expectRevert(abi.encodeWithSelector(IClaimManager.QuoteUnavailable.selector, unquotableToken, payoutToken)); vm.prank(claimer); - uint256 claimId = claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); - - IClaimManager.ClaimRecord memory record = claimManager.claimRecord(POLICY_ID, claimId); - assertEq(uint8(record.status), uint8(IClaimManager.ClaimStatus.Approved), "Claim should be approved"); - assertEq(assertingMock.lastReceivedSlashCount(), 1, "Only one vault should reach executeSlashing"); - assertEq(assertingMock.lastReceivedSlash(0).token, collateralToken, "Only quotable vault should be slashed"); + claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); } function test_fileClaim_WhenTinyVaultBaseSlashRoundsToZero_ExcludesCrossTokenVaultAndSucceeds() public { @@ -2507,25 +2902,28 @@ contract ClaimManagerTest is Test { assertEq(uint8(record.status), uint8(IClaimManager.ClaimStatus.Approved), "Claim should be approved"); } - function test_fileClaim_WhenTinyBaseSlashRoundsToZeroAndQuoterReturnsZeroForOneWei_SkipsVaultInsteadOfReverting() - public - { + function test_fileClaim_WhenDexQuoteDeviatesFromChainlinkByPerTokenDivisor_RevertsQuoteDeviatesFromOracle() public { (address tinyVaultAddr, address tinyToken) = _setupTinyBaseSlashWithRealisticQuoter(); + // Silence unused variable warning + tinyVaultAddr; specMock.setEvaluationResult(true, keccak256("approved")); - vm.expectEmit(true, true, false, false, address(claimManager)); - emit VaultExcludedFromSlashing(tinyVaultAddr, tinyToken); - - vm.prank(claimer); - uint256 claimId = claimManager.fileClaim(POLICY_ID, 1 ether, evidenceHash, ""); - - IClaimManager.ClaimRecord memory record = claimManager.claimRecord(POLICY_ID, claimId); - assertEq( - uint8(record.status), - uint8(IClaimManager.ClaimStatus.Approved), - "Claim should be approved despite tiny vault exclusion" + // The MockSwapperWithPerTokenDivisor divides the quote by 1001, so for tinyStake=1001: + // dex = 1001 / 1001 = 1; chainlinkFair = 1001 (1:1 mock). + // Deviation = (1000 / 1001) * 10000 ≈ 9990 bps > priceDeviationToleranceBps(300) → revert. + vm.expectRevert( + abi.encodeWithSelector( + IClaimManager.QuoteDeviatesFromOracle.selector, + tinyToken, + payoutToken, + uint256(1), // dex quote (1001/1001) + uint256(1001), // chainlink fair (1:1) + uint256(9990) // actual deviationBps + ) ); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, 1 ether, evidenceHash, ""); } function _setupTinyBaseSlashWithRealisticQuoter() private returns (address tinyVaultAddr, address tinyToken) { @@ -2871,6 +3269,7 @@ contract ClaimManagerTest is Test { MockMultiSwapper multiSwapper = new MockMultiSwapper(); multiSwapper.setSwapOutput(collateralToken, requestedAmount); multiSwapper.setSwapOutput(collateralToken2, requestedAmount); + // quoteSwap defaults to 1:1 (amountIn) so oracle deviation gate passes automatically. multiSwapper.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(multiSwapper)); @@ -3639,6 +4038,7 @@ contract MockSwapper { uint256 private _swapOutput; address private _payoutToken; + address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // WETH placeholder receive() external payable {} @@ -3684,45 +4084,88 @@ contract MockPolicy { } } +/** + * @dev Configurable Chainlink price feed mock. + * - All tokens default to a 1:1 price (price = 1e18). + * - `setFeedMissing(token)` makes hasPriceFeed return false and getUSDValue revert. + * - `setShouldRevert(true)` makes every external call revert (staleness simulation). + * - `setPrice(token, price)` overrides the per-token price (in 1e18 scale). + */ contract MockChainlinkPriceFeed { mapping(address => uint256) private _pricePerToken; + mapping(address => bool) private _feedMissing; + bool private _shouldRevert; function setPrice(address token, uint256 price) external { _pricePerToken[token] = price; } + function setFeedMissing(address token) external { + _feedMissing[token] = true; + } + + function setShouldRevert(bool shouldRevert_) external { + _shouldRevert = shouldRevert_; + } + + function hasPriceFeed(address token) external view returns (bool) { + return !_feedMissing[token]; + } + function getUSDValue(address token, uint256 amount) external view returns (uint256) { + require(!_shouldRevert, "MockChainlinkPriceFeed: stale"); + require(!_feedMissing[token], "MockChainlinkPriceFeed: no feed"); uint256 price = _pricePerToken[token]; if (price == 0) { - return amount; + return amount; // 1:1 default } return (amount * price) / 1e18; } function getTokenAmount(address token, uint256 usdValue) external view returns (uint256) { + require(!_shouldRevert, "MockChainlinkPriceFeed: stale"); + require(!_feedMissing[token], "MockChainlinkPriceFeed: no feed"); uint256 price = _pricePerToken[token]; if (price == 0) { - return usdValue; + return usdValue; // 1:1 default } return (usdValue * 1e18) / price; } } +/** + * @dev Multi-token swapper mock supporting separate quoteSwap and executeSwap outputs per token. + * - `setSwapOutput(token, amt)`: sets both quote and execution output (default: amountIn 1:1). + * - `setQuoteOutput(token, amt)`: overrides only the quoteSwap output independently of executeSwap. + * This allows tests to have quoteSwap return 1:1 (passing oracle deviation checks) while executeSwap + * returns a different amount (simulating actual swap price impact). + */ contract MockMultiSwapper { mapping(address => uint256) private _swapOutputs; + mapping(address => uint256) private _quoteOutputs; address private _payoutToken; + address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); function setSwapOutput(address tokenIn, uint256 output) external { _swapOutputs[tokenIn] = output; } + /// @dev Separate quoteSwap amount — allows passing oracle validation (1:1 with chainlink) + /// while executeSwap returns a different amount (simulating price impact). + function setQuoteOutput(address tokenIn, uint256 output) external { + _quoteOutputs[tokenIn] = output; + } + function setPayoutToken(address token) external { _payoutToken = token; } + /// @dev Returns _quoteOutputs if set, otherwise amountIn (1:1 default). + /// The 1:1 default ensures oracle deviation gate passes in tests that only configure executeSwap output. function quoteSwap(address tokenIn, address, uint256 amountIn) external view returns (uint256) { - uint256 out = _swapOutputs[tokenIn]; - return out > 0 ? out : amountIn; + uint256 q = _quoteOutputs[tokenIn]; + if (q > 0) return q; + return amountIn; // 1:1 default: consistent with Chainlink 1:1 mock } function executeSwap(ISwapper.SwapParams calldata params) external returns (uint256) { @@ -3776,6 +4219,7 @@ contract MockSwapperWithUnquotable { mapping(address => bool) private _zeroOnQuote; address private _payoutToken; uint256 private _swapOutput; + address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); function setQuoteReverts(address token) external { _revertOnQuote[token] = true; @@ -3868,6 +4312,7 @@ contract MockSwapperWithTracking { uint256 private _swapOutput; address private _payoutToken; uint256 public lastAmountOutMin; + address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); function setSwapOutput(uint256 output) external { _swapOutput = output; @@ -3897,7 +4342,9 @@ contract MockNativeMultiSwapper { address private constant _NATIVE_ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; mapping(address => uint256) private _swapOutputs; + mapping(address => uint256) private _quoteOutputs; address private _payoutToken; + address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); receive() external payable {} @@ -3905,13 +4352,19 @@ contract MockNativeMultiSwapper { _swapOutputs[tokenIn] = output; } + function setQuoteOutput(address tokenIn, uint256 output) external { + _quoteOutputs[tokenIn] = output; + } + function setPayoutToken(address token) external { _payoutToken = token; } + /// @dev Returns _quoteOutputs if set, otherwise amountIn (1:1 default). function quoteSwap(address tokenIn, address, uint256 amountIn) external view returns (uint256) { - uint256 out = _swapOutputs[tokenIn]; - return out > 0 ? out : amountIn; + uint256 q = _quoteOutputs[tokenIn]; + if (q > 0) return q; + return amountIn; // 1:1 default } function executeSwap(ISwapper.SwapParams calldata params) external payable returns (uint256) { @@ -4045,6 +4498,7 @@ contract MockSwapperWithPerTokenDivisor { mapping(address => uint256) private _divisors; address private _payoutToken; uint256 private _swapOutput; + address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); function setDivisor(address token, uint256 divisor) external { _divisors[token] = divisor; From cafe889692119d22db6d7836b60404625b15b5b7 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Wed, 6 May 2026 17:18:57 +0200 Subject: [PATCH 02/11] fix: silent uint16 truncation of environment variable value --- script/UpgradeClaimManager.s.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/script/UpgradeClaimManager.s.sol b/script/UpgradeClaimManager.s.sol index 0a1ac0b9..bcb2fb03 100644 --- a/script/UpgradeClaimManager.s.sol +++ b/script/UpgradeClaimManager.s.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + import {ClaimManager} from "../src/ClaimManager.sol"; import {UpgradeBase} from "./UpgradeBase.s.sol"; @@ -48,7 +50,7 @@ abstract contract UpgradeClaimManager is UpgradeBase { function upgradeCalldata() internal view override returns (bytes memory) { address chainlinkPriceFeed = vm.envAddress("CHAINLINK_PRICE_FEED"); require(chainlinkPriceFeed != address(0), "CHAINLINK_PRICE_FEED required"); - uint16 toleranceBps = uint16(vm.envOr("PRICE_DEVIATION_TOLERANCE_BPS", uint256(300))); + uint16 toleranceBps = SafeCast.toUint16(vm.envOr("PRICE_DEVIATION_TOLERANCE_BPS", uint256(300))); return abi.encodeCall(ClaimManager.initializeV2, (chainlinkPriceFeed, toleranceBps)); } } From a2d74516523b0495bfac2127613ef64d63eca6e8 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Wed, 6 May 2026 17:49:57 +0200 Subject: [PATCH 03/11] fix: tackle slither issues --- src/ClaimManager.sol | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/ClaimManager.sol b/src/ClaimManager.sol index 2afb7bab..64e2fb06 100644 --- a/src/ClaimManager.sol +++ b/src/ClaimManager.sol @@ -356,18 +356,26 @@ contract ClaimManager is address feed = chainlinkPriceFeed; if (feed == address(0)) revert OracleUnavailable(tokenIn, tokenOut); - address actualIn = tokenIn == _NATIVE_ETH ? ISwapper(swapper).nativeWrapper() : tokenIn; - address actualOut = tokenOut == _NATIVE_ETH ? ISwapper(swapper).nativeWrapper() : tokenOut; + // Resolve NATIVE_ETH sentinel with a single nativeWrapper() call (at most once per invocation). + address nativeWrap = address(0); + if (tokenIn == _NATIVE_ETH || tokenOut == _NATIVE_ETH) { + // slither-disable-next-line calls-loop + nativeWrap = ISwapper(swapper).nativeWrapper(); + } + address actualIn = tokenIn == _NATIVE_ETH ? nativeWrap : tokenIn; + address actualOut = tokenOut == _NATIVE_ETH ? nativeWrap : tokenOut; if (actualIn == actualOut) return amountIn; IChainlinkPriceFeed oracle = IChainlinkPriceFeed(feed); + // slither-disable-next-line calls-loop if (!oracle.hasPriceFeed(actualIn) || !oracle.hasPriceFeed(actualOut)) { revert OracleUnavailable(tokenIn, tokenOut); } - uint256 usd; + uint256 usd = 0; + // slither-disable-next-line calls-loop try oracle.getUSDValue(actualIn, amountIn) returns (uint256 v) { usd = v; } catch { @@ -375,6 +383,7 @@ contract ClaimManager is } if (usd == 0) revert OracleUnavailable(tokenIn, tokenOut); + // slither-disable-next-line calls-loop try oracle.getTokenAmount(actualOut, usd) returns (uint256 v) { fair = v; } catch { @@ -642,7 +651,7 @@ contract ClaimManager is // slither-disable-next-line calls-loop uint256 fair = _chainlinkFairValue(tokens[i], payoutToken, tokenStakes[i]); - uint256 dex; + uint256 dex = 0; // slither-disable-next-line calls-loop try ISwapper(swapper).quoteSwap(tokens[i], payoutToken, tokenStakes[i]) returns (uint256 quoted) { dex = quoted; @@ -799,7 +808,7 @@ contract ClaimManager is function _computeAmountOutMin(address tokenIn, address tokenOut, uint256 amountIn) private returns (uint256) { uint256 fair = _chainlinkFairValue(tokenIn, tokenOut, amountIn); - uint256 dex; + uint256 dex = 0; try ISwapper(swapper).quoteSwap(tokenIn, tokenOut, amountIn) returns (uint256 quoted) { dex = quoted; } catch { From 98eeff72b995debaadd65f6e025e9fecba56fe64 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Fri, 8 May 2026 11:12:47 +0200 Subject: [PATCH 04/11] fix: fix slippage bps --- src/ClaimManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ClaimManager.sol b/src/ClaimManager.sol index 64e2fb06..434abe11 100644 --- a/src/ClaimManager.sol +++ b/src/ClaimManager.sol @@ -411,7 +411,7 @@ contract ClaimManager is view { uint256 diff = dexQuote > chainlinkFair ? dexQuote - chainlinkFair : chainlinkFair - dexQuote; - uint256 deviationBps = (diff * uint256(_MAX_SLIPPAGE_BPS)) / chainlinkFair; + uint256 deviationBps = (diff * 10_000) / chainlinkFair; if (deviationBps > priceDeviationToleranceBps) { revert QuoteDeviatesFromOracle(tokenIn, tokenOut, dexQuote, chainlinkFair, deviationBps); } From cca60d75afa46287ddafb15972eb1a802365bb55 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Fri, 8 May 2026 12:42:49 +0200 Subject: [PATCH 05/11] refactor: rename contract to OraclePriceFeed --- .env.example | 2 +- .env.fork | 2 +- .github/workflows/ci.yml | 2 +- AGENTS.md | 4 +- README.md | 4 +- docs/architecture/01-system-overview.md | 2 +- docs/architecture/02-smart-contracts.md | 6 +- docs/architecture/03-core-integration.md | 8 +- docs/architecture/04-restaker-setup.md | 12 +-- docs/architecture/05-policy-lifecycle.md | 2 +- docs/deployment/Ethereum-Mainnet.md | 2 +- docs/deployment/Sepolia-Testnet.md | 2 +- script/Deploy.s.sol | 14 +-- script/DeployAnvil.s.sol | 6 +- script/DeployEthereum.s.sol | 6 +- script/DeploySepolia.s.sol | 6 +- script/UpgradeClaimManager.s.sol | 10 +- script/VerifySepoliaConfig.s.sol | 34 +++---- script/testing/ForkCompleteCoverageFlow.s.sol | 10 +- script/testing/test-fork.sh | 28 +++--- src/ClaimManager.sol | 76 +++++++-------- src/PolicyManager.sol | 24 ++--- src/interfaces/IClaimManager.sol | 38 ++++---- ...linkPriceFeed.sol => IOraclePriceFeed.sol} | 8 +- src/interfaces/IPolicyManager.sol | 18 ++-- test/defi/CoveredVaultWrapper.fork.t.sol | 8 +- test/defi/helpers/ForkCoverageSetup.sol | 16 ++-- test/defi/helpers/RealCoverageSetup.sol | 14 +-- test/defi/mocks/CoverageMocks.sol | 6 +- test/fuzz/ClaimManager.t.sol | 10 +- test/unit/ClaimManager.t.sol | 96 +++++++++---------- test/unit/PolicyManager.t.sol | 32 +++---- 32 files changed, 245 insertions(+), 263 deletions(-) rename src/interfaces/{IChainlinkPriceFeed.sol => IOraclePriceFeed.sol} (78%) diff --git a/.env.example b/.env.example index bea95acd..6220d3e0 100644 --- a/.env.example +++ b/.env.example @@ -52,7 +52,7 @@ STAKE_MANAGER= SLASHING_MANAGER= REWARDS_MANAGER= SSP_ROUTER= -CHAINLINK_PRICE_FEED= +ORACLE_PRICE_FEED= # ── Token addresses ─────────────────────────────────────────── COLLATERAL_TOKEN= # EigenLayer strategy underlying (e.g. wstETH) diff --git a/.env.fork b/.env.fork index 821adf9f..e142e45b 100644 --- a/.env.fork +++ b/.env.fork @@ -27,7 +27,7 @@ QUOTE_SIGNER_PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e PAYOUT_TOKEN=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 # ── Live Catalysis Core contracts (Ethereum mainnet — docs/deployment/Ethereum-Mainnet.md) ─ -CHAINLINK_PRICE_FEED=0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16 +ORACLE_PRICE_FEED=0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16 STAKE_MANAGER=0x5be5220F81e76e0CF6089fb7E7aE9eF48a8D64Be REWARDS_MANAGER=0x8ae0F0B94fe782D7F055B04d4699c3cc01632b46 SLASHING_MANAGER=0x7Bc39bf135eF3c30E542C196719c91455ff489f0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15984bb6..bb6983c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: SLASHING_MANAGER: "0x0000000000000000000000000000000000000002" STAKE_MANAGER: "0x0000000000000000000000000000000000000003" REWARDS_MANAGER: "0x0000000000000000000000000000000000000004" - CHAINLINK_PRICE_FEED: "0x0000000000000000000000000000000000000005" + ORACLE_PRICE_FEED: "0x0000000000000000000000000000000000000005" run: make deploy-anvil - name: Generate coverage report diff --git a/AGENTS.md b/AGENTS.md index 346d65c9..49e05339 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ Coverage contracts live in `src/`. Tests in `test/unit`, `test/fuzz`, and `test/ ### Interfaces (`src/interfaces/`) -Interfaces mirror each core contract (`ICoverPool`, `IClaimManager`, `ISpecRegistry`, etc.) plus external Catalysis Core dependencies (`ISlashingManager`, `IRewardsManager`, `IChainlinkPriceFeed`, `IStakeManager`). Additional interfaces cover hook callbacks (`IBindPolicyHook`), swap adapter patterns (`IQuotableAdapter`, `IQuoterV2`), evaluation logic (`ISpec`), and token wrappers (`IWETH`). +Interfaces mirror each core contract (`ICoverPool`, `IClaimManager`, `ISpecRegistry`, etc.) plus external Catalysis Core dependencies (`ISlashingManager`, `IRewardsManager`, `IOraclePriceFeed`, `IStakeManager`). Additional interfaces cover hook callbacks (`IBindPolicyHook`), swap adapter patterns (`IQuotableAdapter`, `IQuoterV2`), evaluation logic (`ISpec`), and token wrappers (`IWETH`). ### Swappers (`src/swappers/`) @@ -127,7 +127,7 @@ All network deployments extend `Deploy.s.sol`. The base script deploys contracts **All deployments:** - `PRIVATE_KEY` – deployer key - Network-specific RPC: `SEPOLIA_RPC_URL`, `ETHEREUM_RPC_URL` -- Catalysis Core addresses: `NATIVE_WRAPPER`, `CHAINLINK_PRICE_FEED`, `SLASHING_MANAGER`, `STAKE_MANAGER`, `REWARDS_MANAGER` +- Catalysis Core addresses: `NATIVE_WRAPPER`, `ORACLE_PRICE_FEED`, `SLASHING_MANAGER`, `STAKE_MANAGER`, `REWARDS_MANAGER` **Optional role overrides** (default to deployer/admin): - `ADMIN`, `PLATFORM_TREASURY`, `COVER_POOL_FACTORY_CREATOR`, `SWAP_MANAGER`, `PLATFORM_FEE_BPS` diff --git a/README.md b/README.md index f95ff2c9..4d23b6eb 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ External dependencies from Catalysis Core: | `SlashingManager` | Collateral slashing execution across restaking networks. | | `StakeManager` | Manages restaker stake and delegation. | | `RewardsManager` | Distributes rewards to restakers. | -| `ChainlinkPriceFeed` | Converts token amounts to USD values for slashing calculations. | +| `OraclePriceFeed` | Converts token amounts to USD values for slashing calculations. | ### Swappers (`src/swappers/`) @@ -169,7 +169,7 @@ See [`.env.example`](.env.example) for all variables with inline documentation. | Variable | Description | |----------|-------------| | `NATIVE_WRAPPER` | WETH or equivalent native token wrapper | -| `CHAINLINK_PRICE_FEED` | ChainlinkPriceFeed contract | +| `ORACLE_PRICE_FEED` | OraclePriceFeed contract | | `SLASHING_MANAGER` | SlashingManager from Catalysis Core | | `STAKE_MANAGER` | StakeManager from Catalysis Core | | `REWARDS_MANAGER` | RewardsManager from Catalysis Core | diff --git a/docs/architecture/01-system-overview.md b/docs/architecture/01-system-overview.md index 2604931c..459c64db 100644 --- a/docs/architecture/01-system-overview.md +++ b/docs/architecture/01-system-overview.md @@ -61,7 +61,7 @@ graph TB SM[StakeManager] RM[RewardsManager] SLM[SlashingManager] - PF[ChainlinkPriceFeed] + PF[OraclePriceFeed] end subgraph Restaking diff --git a/docs/architecture/02-smart-contracts.md b/docs/architecture/02-smart-contracts.md index 56eeee03..2b9ff8e0 100644 --- a/docs/architecture/02-smart-contracts.md +++ b/docs/architecture/02-smart-contracts.md @@ -19,7 +19,7 @@ graph LR SW[Swapper] UV3[UniswapV3Adapter] CVW[CoveredVaultWrapper] - PF[ChainlinkPriceFeed] + PF[OraclePriceFeed] CPF --> CP PM --> CP @@ -193,7 +193,7 @@ Note: `PolicyManager` stores a `claimManager` address for reference but does **n - otherwise: swap through `Swapper` using `quoteSwap`-derived `amountOutMin`; surplus forwarded similarly. - Surplus routing is wrapped in a `try`/`catch`; failures emit `SwapSurplusDistributionFailed` and leave tokens in the contract for recovery via `recoverTokens`. -No USD conversion or `ChainlinkPriceFeed` is used in the claim execution path. +No USD conversion or `OraclePriceFeed` is used in the claim execution path. ### 6) SpecRegistry @@ -309,7 +309,7 @@ Coverage contracts rely on Core interfaces: - `IStakeManager` for committee create/operator/vault and stake reads, - `IRewardsManager` for reward fan-out trigger, - `ISlashingManager` for committee slashing execution, -- `IChainlinkPriceFeed` for stake-to-USD conversion at bind time (stake sufficiency check in `PolicyManager`). +- `IOraclePriceFeed` for stake-to-USD conversion at bind time (stake sufficiency check in `PolicyManager`). ## Next Steps diff --git a/docs/architecture/03-core-integration.md b/docs/architecture/03-core-integration.md index 69f94e86..e58b558a 100644 --- a/docs/architecture/03-core-integration.md +++ b/docs/architecture/03-core-integration.md @@ -19,7 +19,7 @@ This separation keeps restaking platform complexity out of Coverage contracts. | `CoverPool` | `StakeManager` | `setCommitteeVaults(policyId, vaults[], coverageLimit)` | Bind selected vaults/strategies; configures TVL limits on EigenLayer vaults | | `PolicyManager` | `StakeManager` | `getCommitteeVaults(policyId)` | Verify vault backing exists before bind finalization | | `PolicyManager` | `StakeManager` | `getCommitteeTokenStakes(policyId)` | Retrieve per-vault token-native stake amounts for USD sufficiency check | -| `PolicyManager` | `ChainlinkPriceFeed` | `getUSDValue(token, amount)` | Convert `coverageLimit` and committee token stakes to USD for bind-time sufficiency check | +| `PolicyManager` | `OraclePriceFeed` | `getUSDValue(token, amount)` | Convert `coverageLimit` and committee token stakes to USD for bind-time sufficiency check | | `PremiumManager` | `RewardsManager` | `distributeRewards(policyId, operator, amount, token, taskId)` | Trigger Core reward fan-out; RewardsManager pulls restaker share from PremiumManager, then routes via SSPRouter | | `ClaimManager` | `SlashingManager` | `previewSlashing(committeeId, operator)` | Read per-vault token-native stake amounts before executing | | `ClaimManager` | `SlashingManager` | `executeSlashing(committeeId, operator, VaultSlash[], taskId)` | Execute token-native per-vault slashing and return seized collateral | @@ -81,7 +81,7 @@ Sequence: - `SSPRouter._configureEigenStrategies` runs internally: adds strategies to operator set, sets TVL limits (converting `coverageLimit` USD to token-native amounts), and locks duration vaults. 3. `PolicyManager.bindPolicy(...)` checks: - committee has at least one vault (`getCommitteeVaults`), - - `getCommitteeTokenStakes(policyId)` returns per-vault stakes; their USD values (via `ChainlinkPriceFeed.getUSDValue`) sum to >= `coverageLimit` USD-equivalent. + - `getCommitteeTokenStakes(policyId)` returns per-vault stakes; their USD values (via `OraclePriceFeed.getUSDValue`) sum to >= `coverageLimit` USD-equivalent. Outcome: @@ -129,7 +129,7 @@ No-shortfall withdrawal path: - Slash amounts passed to `executeSlashing` are **token-native** (not USD); `ClaimManager` computes them via `previewSlashing` + `quoteSwap`. - Coverage payouts are token-denominated by `payoutToken`. -- `coverageLimit` in `Quote`/`BoundPolicy`/`PolicyMetadata` is denominated in **`payoutToken` native units** (not USD). At bind time, `PolicyManager.bindPolicy` converts it to USD via `ChainlinkPriceFeed.getUSDValue(payoutToken, coverageLimit)` and compares against the committee's total staked USD. This is the only point where `ChainlinkPriceFeed` is used in Coverage contracts. The `PolicyDraft` struct does not contain a `coverageAmount` field. +- `coverageLimit` in `Quote`/`BoundPolicy`/`PolicyMetadata` is denominated in **`payoutToken` native units** (not USD). At bind time, `PolicyManager.bindPolicy` converts it to USD via `OraclePriceFeed.getUSDValue(payoutToken, coverageLimit)` and compares against the committee's total staked USD. This is the only point where `OraclePriceFeed` is used in Coverage contracts. The `PolicyDraft` struct does not contain a `coverageAmount` field. ## Trust and Failure Boundaries @@ -151,7 +151,7 @@ Failure patterns: - Ensure restakers have deposited into the committee's duration vault before bind. Duration vaults act as EigenLayer operators — no separate magnitude allocation or operator-set registration is needed. See [Restaker Setup](./04-restaker-setup.md). - Register and verify vault module mappings in Core (`SSPRouter.registerVaultModule`) before bind. - Configure swap routes for expected collateral → payout token pairs before claims; `Swapper.quoteSwap` is used at claim time for slippage computation. -- `WETH` price feed must be registered in `ChainlinkPriceFeed` before binding policies whose payout token is WETH (stake USD sufficiency check at bind). +- `WETH` price feed must be registered in `OraclePriceFeed` before binding policies whose payout token is WETH (stake USD sufficiency check at bind). ## Next Steps diff --git a/docs/architecture/04-restaker-setup.md b/docs/architecture/04-restaker-setup.md index f4d6763d..3a657c59 100644 --- a/docs/architecture/04-restaker-setup.md +++ b/docs/architecture/04-restaker-setup.md @@ -9,7 +9,7 @@ duration vault (and optionally a Symbiotic vault). The restaker deposit is performed directly against EigenLayer contracts (no Coverage script needed). Stake sufficiency is enforced at bind time by `PolicyManager.bindPolicy`, which reads `StakeManager.getCommitteeTokenStakes(policyId)` and converts each vault's token-native stake to USD -via `ChainlinkPriceFeed.getUSDValue`, then requires the total >= `coverageLimit` in USD. +via `OraclePriceFeed.getUSDValue`, then requires the total >= `coverageLimit` in USD. ## Duration Vault Model (EigenLayer) @@ -61,7 +61,7 @@ The deposited amount must satisfy the coverage USD requirement. The USD value is `EigenAdapter.getStrategiesStakeUSD(vaults)` at bind time: ``` -stake_USD = totalShares * sharesToUnderlyingView() * ChainlinkPriceFeed.getUSDValue(token, 1e18) / 1e18 +stake_USD = totalShares * sharesToUnderlyingView() * OraclePriceFeed.getUSDValue(token, 1e18) / 1e18 ``` Example: to cover a `coverageLimit` equivalent to 1000 USD (at 8 decimals: `100_000_000_000` payout-token units, given a 1:1 price), at least `$1000 / ETH_USD_PRICE` ETH worth of WETH must be deposited. @@ -75,7 +75,7 @@ These steps are performed by the curator via `CoverPool.bindPolicyForRequest(... sets TVL limits on the duration vault (converting `coverageLimit` USD to token-native amounts), and locks the duration vault. 2. calls `PolicyManager.bindPolicy(...)` — verifies committee has at least one vault, reads - `getCommitteeTokenStakes(policyId)`, converts total stake to USD via `ChainlinkPriceFeed.getUSDValue`, + `getCommitteeTokenStakes(policyId)`, converts total stake to USD via `OraclePriceFeed.getUSDValue`, and requires `totalStakeUSD >= coverageLimitUSD` before activating the policy. ## Why a Separate Setup Phase Exists @@ -131,12 +131,12 @@ StakeManager.getCommitteeTokenStakes(policyId) Then for each vault with non-zero stake: ``` -totalStakeUSD += ChainlinkPriceFeed.getUSDValue(tokens[i], tokenStakes[i]) +totalStakeUSD += OraclePriceFeed.getUSDValue(tokens[i], tokenStakes[i]) ``` And the coverage limit in USD: ``` -coverageLimitUSD = ChainlinkPriceFeed.getUSDValue(payoutToken, coverageLimit) +coverageLimitUSD = OraclePriceFeed.getUSDValue(payoutToken, coverageLimit) ``` Bind reverts with `InsufficientStake` if `totalStakeUSD < coverageLimitUSD`. @@ -186,7 +186,7 @@ Duration vault addresses are looked up at runtime via `StakeManager.getCommittee |---------|-----------|-----| | `InsufficientStake` at bind | `totalStakeUSD` < `coverageLimitUSD` in `PolicyManager.bindPolicy` | Deposit more collateral or reduce `coverageLimit` in the quote | | `VaultModuleNotSet` at `setCommitteeVaults` | Duration vault not registered in `SSPRouter` | Core admin: `SSPRouter.registerVaultModule(vault, 2)` | -| `PriceFeedNotFound` at bind | Token not registered in `ChainlinkPriceFeed` (used only at bind time in `PolicyManager.bindPolicy`; not used during `fileClaim`) | Core admin: `registerPriceFeed(token, aggregator, stalenessThreshold)` | +| `PriceFeedNotFound` at bind | Token not registered in `OraclePriceFeed` (used only at bind time in `PolicyManager.bindPolicy`; not used during `fileClaim`) | Core admin: `registerPriceFeed(token, aggregator, stalenessThreshold)` | | `InvalidCommitteeId` | `policyId = 0` (not set from output of RequestCoverage) | Set `POLICY_ID` env var from `RequestCoverage.s.sol` output | | Stake reads 0 before bind | Vaults not yet added to committee (expected) | Stake = 0 until `bindPolicyForRequest` runs `setCommitteeVaults` | | `RewardsManager safeTransferFrom` reverts on distribution | Unexpected: `PremiumManager` uses ephemeral `forceApprove` internally before each `distributeRewards` call — no admin allowance setup is required | Check that `rewardsManager` is correctly configured in `PremiumManager` | diff --git a/docs/architecture/05-policy-lifecycle.md b/docs/architecture/05-policy-lifecycle.md index 398e6717..e22dd5c8 100644 --- a/docs/architecture/05-policy-lifecycle.md +++ b/docs/architecture/05-policy-lifecycle.md @@ -103,7 +103,7 @@ Entry: Curator (pool admin) calls `CoverPool.bindPolicyForRequest(...)`. - request fields (`buyer`, `beneficiary`, `claimer`, `payoutToken`) match the stored draft, - `policyId` matches `quote.policyId`, - committee has at least one vault, -- committee total stake (USD) >= `coverageLimit` (payoutToken-denominated) converted to USD via `ChainlinkPriceFeed.getUSDValue`, +- committee total stake (USD) >= `coverageLimit` (payoutToken-denominated) converted to USD via `OraclePriceFeed.getUSDValue`, - policy commit is unique. Note: quote expiry and EIP-712 signature verification are performed exclusively in `CoverPool._verifyQuote`; `PolicyManager.bindPolicy` does not re-verify the signature even though the `BindPolicyRequest` struct carries the `signature` field. diff --git a/docs/deployment/Ethereum-Mainnet.md b/docs/deployment/Ethereum-Mainnet.md index b1819c77..9f45fde8 100644 --- a/docs/deployment/Ethereum-Mainnet.md +++ b/docs/deployment/Ethereum-Mainnet.md @@ -67,7 +67,7 @@ - Proxy address: [0xe6168092892E545701D92BEe10149178bE0EEDF4](https://etherscan.io/address/0xe6168092892E545701D92BEe10149178bE0EEDF4) - Implementation address: [0xfaffe50bdccdecffddfdaf812ac8906765bfb4f6](https://etherscan.io/address/0xfaffe50bdccdecffddfdaf812ac8906765bfb4f6) -### ChainlinkPriceFeed +### OraclePriceFeed - Proxy address: [0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16](https://etherscan.io/address/0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16) - Implementation address: [0x99bba889b9b4df3d62e1ec8f9e03c48b97b580f6](https://etherscan.io/address/0x99bba889b9b4df3d62e1ec8f9e03c48b97b580f6) diff --git a/docs/deployment/Sepolia-Testnet.md b/docs/deployment/Sepolia-Testnet.md index 11da3cf4..8d8f0df2 100644 --- a/docs/deployment/Sepolia-Testnet.md +++ b/docs/deployment/Sepolia-Testnet.md @@ -63,7 +63,7 @@ - Proxy address: [0xe6168092892E545701D92BEe10149178bE0EEDF4](https://sepolia.etherscan.io/address/0xe6168092892E545701D92BEe10149178bE0EEDF4) - Implementation address: [0xfaffe50bdccdecffddfdaf812ac8906765bfb4f6](https://sepolia.etherscan.io/address/0xfaffe50bdccdecffddfdaf812ac8906765bfb4f6) -### ChainlinkPriceFeed +### OraclePriceFeed - Proxy address: [0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16](https://sepolia.etherscan.io/address/0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16) - Implementation address: [0x99bba889b9b4df3d62e1ec8f9e03c48b97b580f6](https://sepolia.etherscan.io/address/0x99bba889b9b4df3d62e1ec8f9e03c48b97b580f6) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index cb2ce7a9..22cc683c 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -44,7 +44,7 @@ import {CreateXDeployer} from "./CreateXDeployer.s.sol"; * - SLASHING_MANAGER: Catalysis Core SlashingManager address (required) * - STAKE_MANAGER: Catalysis Core StakeManager address (required) * - REWARDS_MANAGER: Catalysis Core RewardsManager address (required) - * - CHAINLINK_PRICE_FEED: ChainlinkPriceFeed address (required) + * - ORACLE_PRICE_FEED: OraclePriceFeed address (required) * - PLATFORM_TREASURY: Platform fee recipient (defaults to admin) * - PLATFORM_FEE_BPS: Platform fee in basis points (defaults to 500 = 5%) * - COVER_POOL_FACTORY_CREATOR: Factory creator role (defaults to admin) @@ -69,14 +69,14 @@ abstract contract Deploy is CreateXDeployer { * @notice External contract addresses required for deployment. * @param nativeWrapper WETH or equivalent native token wrapper address. * @param slashingManager SlashingManager contract address from Catalysis Core. - * @param chainlinkPriceFeed ChainlinkPriceFeed contract address from Catalysis Core. + * @param oraclePriceFeed OraclePriceFeed contract address from Catalysis Core. * @param stakeManager StakeManager contract address from Catalysis Core. * @param rewardsManager RewardsManager contract address from Catalysis Core. */ struct ExternalConfig { address nativeWrapper; address slashingManager; - address chainlinkPriceFeed; + address oraclePriceFeed; address stakeManager; address rewardsManager; } @@ -181,7 +181,7 @@ abstract contract Deploy is CreateXDeployer { return ExternalConfig({ nativeWrapper: vm.envOr("NATIVE_WRAPPER", address(0)), slashingManager: vm.envOr("SLASHING_MANAGER", address(0)), - chainlinkPriceFeed: vm.envOr("CHAINLINK_PRICE_FEED", address(0)), + oraclePriceFeed: vm.envOr("ORACLE_PRICE_FEED", address(0)), stakeManager: vm.envOr("STAKE_MANAGER", address(0)), rewardsManager: vm.envOr("REWARDS_MANAGER", address(0)) }); @@ -393,7 +393,7 @@ abstract contract Deploy is CreateXDeployer { external_.stakeManager, roles.deployer, // Temporary placeholder for claimManager, will be set after ClaimManager deployment roles.deployer, // Temporary placeholder for coverPoolFactory, will be set after factory deployment - external_.chainlinkPriceFeed, + external_.oraclePriceFeed, getInitialPayoutTokens() ) ) @@ -427,7 +427,7 @@ abstract contract Deploy is CreateXDeployer { ); ClaimManager(payable(contracts.claimManagerProxy)).setPremiumManager(contracts.premiumManagerProxy); - ClaimManager(payable(contracts.claimManagerProxy)).initializeV2(external_.chainlinkPriceFeed, 300); + ClaimManager(payable(contracts.claimManagerProxy)).initializeV2(external_.oraclePriceFeed, 300); PolicyManager(contracts.policyManagerProxy).setClaimManager(contracts.claimManagerProxy); @@ -617,7 +617,7 @@ abstract contract Deploy is CreateXDeployer { external_ = getExternalConfig(); require(external_.nativeWrapper != address(0), "Native wrapper address required"); require(external_.slashingManager != address(0), "SLASHING_MANAGER required"); - require(external_.chainlinkPriceFeed != address(0), "CHAINLINK_PRICE_FEED required"); + require(external_.oraclePriceFeed != address(0), "ORACLE_PRICE_FEED required"); require(external_.stakeManager != address(0), "STAKE_MANAGER required"); require(external_.rewardsManager != address(0), "REWARDS_MANAGER required"); } diff --git a/script/DeployAnvil.s.sol b/script/DeployAnvil.s.sol index 4f78feec..153ea5bf 100644 --- a/script/DeployAnvil.s.sol +++ b/script/DeployAnvil.s.sol @@ -10,11 +10,11 @@ import {Deploy} from "./Deploy.s.sol"; * - SLASHING_MANAGER: Address of SlashingManager from Catalysis Core * - STAKE_MANAGER: Address of StakeManager from Catalysis Core * - REWARDS_MANAGER: Address of RewardsManager from Catalysis Core - * - CHAINLINK_PRICE_FEED: Address of ChainlinkPriceFeed + * - ORACLE_PRICE_FEED: Address of OraclePriceFeed * * Usage: * SLASHING_MANAGER=0x... STAKE_MANAGER=0x... REWARDS_MANAGER=0x... - * CHAINLINK_PRICE_FEED=0x... \ + * ORACLE_PRICE_FEED=0x... \ * forge script script/DeployAnvil.s.sol --broadcast --rpc-url http://localhost:8545 */ contract DeployAnvil is Deploy { @@ -43,7 +43,7 @@ contract DeployAnvil is Deploy { return ExternalConfig({ nativeWrapper: WETH_ANVIL, slashingManager: vm.envAddress("SLASHING_MANAGER"), - chainlinkPriceFeed: vm.envAddress("CHAINLINK_PRICE_FEED"), + oraclePriceFeed: vm.envAddress("ORACLE_PRICE_FEED"), stakeManager: vm.envAddress("STAKE_MANAGER"), rewardsManager: vm.envAddress("REWARDS_MANAGER") }); diff --git a/script/DeployEthereum.s.sol b/script/DeployEthereum.s.sol index c1895879..2a87f635 100644 --- a/script/DeployEthereum.s.sol +++ b/script/DeployEthereum.s.sol @@ -11,13 +11,13 @@ import {Deploy} from "./Deploy.s.sol"; * - SLASHING_MANAGER: Address of SlashingManager from Catalysis Core * - STAKE_MANAGER: Address of StakeManager from Catalysis Core * - REWARDS_MANAGER: Address of RewardsManager from Catalysis Core - * - CHAINLINK_PRICE_FEED: Address of ChainlinkPriceFeed + * - ORACLE_PRICE_FEED: Address of OraclePriceFeed * - PAYOUT_TOKENS: Comma-separated payout token addresses to whitelist on PolicyManager * - PREMIUM_TOKENS: Comma-separated premium token addresses to approve on PremiumManager * * Usage: * SLASHING_MANAGER=0x... STAKE_MANAGER=0x... REWARDS_MANAGER=0x... - * CHAINLINK_PRICE_FEED=0x... PAYOUT_TOKENS=0x... PREMIUM_TOKENS=0x... \ + * ORACLE_PRICE_FEED=0x... PAYOUT_TOKENS=0x... PREMIUM_TOKENS=0x... \ * forge script script/DeployEthereum.s.sol --broadcast --rpc-url mainnet --verify */ contract DeployEthereum is Deploy { @@ -42,7 +42,7 @@ contract DeployEthereum is Deploy { return ExternalConfig({ nativeWrapper: WETH_MAINNET, slashingManager: vm.envAddress("SLASHING_MANAGER"), - chainlinkPriceFeed: vm.envAddress("CHAINLINK_PRICE_FEED"), + oraclePriceFeed: vm.envAddress("ORACLE_PRICE_FEED"), stakeManager: vm.envAddress("STAKE_MANAGER"), rewardsManager: vm.envAddress("REWARDS_MANAGER") }); diff --git a/script/DeploySepolia.s.sol b/script/DeploySepolia.s.sol index bdb11914..6f6cc662 100644 --- a/script/DeploySepolia.s.sol +++ b/script/DeploySepolia.s.sol @@ -11,13 +11,13 @@ import {Deploy} from "./Deploy.s.sol"; * - SLASHING_MANAGER: Address of SlashingManager from Catalysis Core * - STAKE_MANAGER: Address of StakeManager from Catalysis Core * - REWARDS_MANAGER: Address of RewardsManager from Catalysis Core - * - CHAINLINK_PRICE_FEED: Address of ChainlinkPriceFeed + * - ORACLE_PRICE_FEED: Address of OraclePriceFeed * - PAYOUT_TOKENS: Comma-separated payout token addresses to whitelist on PolicyManager * - PREMIUM_TOKENS: Comma-separated premium token addresses to approve on PremiumManager * * Usage: * SLASHING_MANAGER=0x... STAKE_MANAGER=0x... REWARDS_MANAGER=0x... - * CHAINLINK_PRICE_FEED=0x... PAYOUT_TOKENS=0x... PREMIUM_TOKENS=0x... \ + * ORACLE_PRICE_FEED=0x... PAYOUT_TOKENS=0x... PREMIUM_TOKENS=0x... \ * forge script script/DeploySepolia.s.sol --broadcast --rpc-url sepolia --verify */ contract DeploySepolia is Deploy { @@ -42,7 +42,7 @@ contract DeploySepolia is Deploy { return ExternalConfig({ nativeWrapper: WETH_SEPOLIA, slashingManager: vm.envAddress("SLASHING_MANAGER"), - chainlinkPriceFeed: vm.envAddress("CHAINLINK_PRICE_FEED"), + oraclePriceFeed: vm.envAddress("ORACLE_PRICE_FEED"), stakeManager: vm.envAddress("STAKE_MANAGER"), rewardsManager: vm.envAddress("REWARDS_MANAGER") }); diff --git a/script/UpgradeClaimManager.s.sol b/script/UpgradeClaimManager.s.sol index bcb2fb03..28b6964f 100644 --- a/script/UpgradeClaimManager.s.sol +++ b/script/UpgradeClaimManager.s.sol @@ -18,8 +18,8 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * Environment Variables: * - PRIVATE_KEY: Deployer private key with DEFAULT_ADMIN_ROLE on ClaimManager * - CLAIM_MANAGER_PROXY: Address of the deployed ClaimManager proxy (required) - * - CHAINLINK_PRICE_FEED: Address of the ChainlinkPriceFeed contract (required) - * - PRICE_DEVIATION_TOLERANCE_BPS: Max DEX-vs-Chainlink deviation tolerance in bps + * - ORACLE_PRICE_FEED: Address of the OraclePriceFeed contract (required) + * - PRICE_DEVIATION_TOLERANCE_BPS: Max DEX-vs-oracle deviation tolerance in bps * (optional, defaults to 300 = 3%) */ abstract contract UpgradeClaimManager is UpgradeBase { @@ -48,9 +48,9 @@ abstract contract UpgradeClaimManager is UpgradeBase { * the V2 storage fields in the same transaction as the implementation swap. */ function upgradeCalldata() internal view override returns (bytes memory) { - address chainlinkPriceFeed = vm.envAddress("CHAINLINK_PRICE_FEED"); - require(chainlinkPriceFeed != address(0), "CHAINLINK_PRICE_FEED required"); + address oraclePriceFeed = vm.envAddress("ORACLE_PRICE_FEED"); + require(oraclePriceFeed != address(0), "ORACLE_PRICE_FEED required"); uint16 toleranceBps = SafeCast.toUint16(vm.envOr("PRICE_DEVIATION_TOLERANCE_BPS", uint256(300))); - return abi.encodeCall(ClaimManager.initializeV2, (chainlinkPriceFeed, toleranceBps)); + return abi.encodeCall(ClaimManager.initializeV2, (oraclePriceFeed, toleranceBps)); } } diff --git a/script/VerifySepoliaConfig.s.sol b/script/VerifySepoliaConfig.s.sol index 4e7d1fda..97f17202 100644 --- a/script/VerifySepoliaConfig.s.sol +++ b/script/VerifySepoliaConfig.s.sol @@ -37,7 +37,7 @@ interface ISSPRouterView { function getVaultModule(address vault) external view returns (uint8); } -interface IChainlinkPriceFeedView { +interface IOraclePriceFeedView { function getUSDValue(address token, uint256 amount) external view returns (uint256); } @@ -47,13 +47,13 @@ interface IAdapterView { interface IPolicyManagerView { function stakeManager() external view returns (address); - function chainlinkPriceFeed() external view returns (address); + function oraclePriceFeed() external view returns (address); } interface IClaimManagerView { function slashingManager() external view returns (address); function premiumManager() external view returns (address); - function chainlinkPriceFeed() external view returns (address); + function oraclePriceFeed() external view returns (address); function priceDeviationToleranceBps() external view returns (uint16); } @@ -80,7 +80,7 @@ interface ICoverPoolFactoryView { * Coverage: * Flow 1 – CreateCoverPool: CoverPoolFactory not paused, stakeManager wired * Flow 2-3– RequestCoverage: StakeManager roles + wiring, SSPRouter adapters - * Flow 4 – PrepareBindPolicy: Adapter chainlinkPriceFeed, SSPRouter adapters + * Flow 4 – PrepareBindPolicy: Adapter oraclePriceFeed, SSPRouter adapters * Flow 5 – BindPolicy: SSPRouter vault module registration * Flow 6-7– DistributePremium: RewardsManager wiring + PREMIUM_MANAGER_ROLE * Flow 8 – FileClaim: SlashingManager wiring + ClaimManager↔PremiumManager @@ -107,7 +107,7 @@ contract VerifySepoliaConfig is Script { address internal constant REWARDS_MANAGER = 0x6A0823fC61F970BB8a52cF95BfB8F32265Ada8b8; address internal constant SSP_ROUTER = 0xf68c72E92f9BdB7Cdba2AF4026Bab8DC22781148; address internal constant EIGEN_ADAPTER = 0xB4bB09dcE1C1D9d987D5dc0dC2c8770D4fbBD49b; - address internal constant CHAINLINK_PRICE_FEED = 0x2b48F18f73C66075168b727524c99aFE12814417; + address internal constant ORACLE_PRICE_FEED = 0x2b48F18f73C66075168b727524c99aFE12814417; // ── Known E2E vault (EigenLayer wstETH DurationVaultStrategy, deployed by SSPRouter) ── address internal constant WSTETH_EL_STRATEGY = 0x7db0C347645bE3E19Af87d72E6220361cdD2428D; @@ -198,9 +198,7 @@ contract VerifySepoliaConfig is Script { _check( "SSPRouter.getAdapter(EIGENLAYER)", ISSPRouterView(SSP_ROUTER).getAdapter(MODULE_EIGENLAYER), EIGEN_ADAPTER ); - _check( - "EigenAdapter.chainlinkPriceFeed", IAdapterView(EIGEN_ADAPTER).chainlinkPriceFeed(), CHAINLINK_PRICE_FEED - ); + _check("EigenAdapter.chainlinkPriceFeed", IAdapterView(EIGEN_ADAPTER).chainlinkPriceFeed(), ORACLE_PRICE_FEED); console2.log(""); } @@ -213,11 +211,7 @@ contract VerifySepoliaConfig is Script { function _checkFlow5_bindPolicy() internal { console2.log("--- Flow 5: BindPolicy ---"); - _check( - "PolicyManager.chainlinkPriceFeed", - IPolicyManagerView(POLICY_MANAGER).chainlinkPriceFeed(), - CHAINLINK_PRICE_FEED - ); + _check("PolicyManager.oraclePriceFeed", IPolicyManagerView(POLICY_MANAGER).oraclePriceFeed(), ORACLE_PRICE_FEED); // addCommitteeVaults checks getVaultModule != NONE for each vault uint8 module = ISSPRouterView(SSP_ROUTER).getVaultModule(WSTETH_EL_STRATEGY); @@ -280,11 +274,7 @@ contract VerifySepoliaConfig is Script { _check("ClaimManager.slashingManager", IClaimManagerView(CLAIM_MANAGER).slashingManager(), SLASHING_MANAGER); _check("ClaimManager.premiumManager", IClaimManagerView(CLAIM_MANAGER).premiumManager(), PREMIUM_MANAGER); - _check( - "ClaimManager.chainlinkPriceFeed", - IClaimManagerView(CLAIM_MANAGER).chainlinkPriceFeed(), - CHAINLINK_PRICE_FEED - ); + _check("ClaimManager.oraclePriceFeed", IClaimManagerView(CLAIM_MANAGER).oraclePriceFeed(), ORACLE_PRICE_FEED); uint16 toleranceBps = IClaimManagerView(CLAIM_MANAGER).priceDeviationToleranceBps(); if (toleranceBps > 0) { console2.log("[OK] ClaimManager.priceDeviationToleranceBps:", toleranceBps, "bps"); @@ -307,15 +297,15 @@ contract VerifySepoliaConfig is Script { _check("SSPRouter.slashingManager", ISSPRouterView(SSP_ROUTER).slashingManager(), SLASHING_MANAGER); _check("SSPRouter.stakeManager", ISSPRouterView(SSP_ROUTER).stakeManager(), STAKE_MANAGER); - try IChainlinkPriceFeedView(CHAINLINK_PRICE_FEED).getUSDValue(WETH, 1e18) returns (uint256 usdValue) { + try IOraclePriceFeedView(ORACLE_PRICE_FEED).getUSDValue(WETH, 1e18) returns (uint256 usdValue) { if (usdValue > 0) { - console2.log("[OK] ChainlinkPriceFeed.getUSDValue(WETH, 1e18):", usdValue, "(USD, 8 dec)"); + console2.log("[OK] OraclePriceFeed.getUSDValue(WETH, 1e18):", usdValue, "(USD, 8 dec)"); } else { - console2.log("[FAIL] ChainlinkPriceFeed.getUSDValue(WETH) returned 0 -- WETH feed not registered"); + console2.log("[FAIL] OraclePriceFeed.getUSDValue(WETH) returned 0 -- WETH feed not registered"); _failures++; } } catch { - console2.log("[FAIL] ChainlinkPriceFeed.getUSDValue(WETH) reverted -- WETH feed not registered"); + console2.log("[FAIL] OraclePriceFeed.getUSDValue(WETH) reverted -- WETH feed not registered"); _failures++; } diff --git a/script/testing/ForkCompleteCoverageFlow.s.sol b/script/testing/ForkCompleteCoverageFlow.s.sol index 649c9164..7e240fbf 100644 --- a/script/testing/ForkCompleteCoverageFlow.s.sol +++ b/script/testing/ForkCompleteCoverageFlow.s.sol @@ -46,8 +46,8 @@ interface IDurationVaultStrategyFork { function underlyingToken() external view returns (address); } -/// @notice Queries USD values from the live Catalysis ChainlinkPriceFeed. -interface IChainlinkPriceFeedFork { +/// @notice Queries USD values from the live Catalysis OraclePriceFeed. +interface IOraclePriceFeedFork { function getUSDValue(address token, uint256 amount) external view returns (uint256); } @@ -220,8 +220,8 @@ contract ForkCompleteCoverageFlow is CompleteCoverageFlow { console.log("\n=== PHASE 3b: FUND VAULTS (EigenLayer stake) ==="); - address chainlinkPriceFeed = vm.envAddress("CHAINLINK_PRICE_FEED"); - uint256 coverageLimitUSD = IChainlinkPriceFeedFork(chainlinkPriceFeed).getUSDValue(payoutToken, coverageLimit); + address oraclePriceFeed = vm.envAddress("ORACLE_PRICE_FEED"); + uint256 coverageLimitUSD = IOraclePriceFeedFork(oraclePriceFeed).getUSDValue(payoutToken, coverageLimit); uint256 targetUSD = coverageLimitUSD * 2; for (uint256 i = 0; i < vaults.length; i++) { @@ -236,7 +236,7 @@ contract ForkCompleteCoverageFlow is CompleteCoverageFlow { ); uint256 tokenUnit = 10 ** uint256(IERC20Metadata(collateralToken).decimals()); - uint256 tokenPriceUSD = IChainlinkPriceFeedFork(chainlinkPriceFeed).getUSDValue(collateralToken, tokenUnit); + uint256 tokenPriceUSD = IOraclePriceFeedFork(oraclePriceFeed).getUSDValue(collateralToken, tokenUnit); uint256 collateralAmount = (targetUSD * tokenUnit + tokenPriceUSD - 1) / tokenPriceUSD; uint256 depositorKey = _anvilDepositorKey(i); diff --git a/script/testing/test-fork.sh b/script/testing/test-fork.sh index 11009bb7..a27c9f15 100755 --- a/script/testing/test-fork.sh +++ b/script/testing/test-fork.sh @@ -4,7 +4,7 @@ # Sequence: # 1. Start an Anvil fork of mainnet # 2. Use live Catalysis Core contracts; grant required roles to deployer via MASTER_ADMIN impersonation -# 2b. Register USDC/USD price feed if not already registered on live ChainlinkPriceFeed +# 2b. Register USDC/USD price feed if not already registered on live OraclePriceFeed # 3. Deploy Catalysis Coverage on the fork (fresh each run) # 3b. Restore deployer DEFAULT_ADMIN_ROLE on Coverage contracts via TimelockController impersonation # 3c. Bump PolicyManager._nextPolicyId above existing live committee IDs (avoids CommitteeAlreadyExists) @@ -24,7 +24,7 @@ # SLASHING_MANAGER = 0x7Bc39bf135eF3c30E542C196719c91455ff489f0 # REWARDS_MANAGER = 0x8ae0F0B94fe782D7F055B04d4699c3cc01632b46 # SSP_ROUTER = 0xF39E592E93A7a925a57464e0120B555A25590486 -# CHAINLINK_PRICE_FEED = 0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16 +# ORACLE_PRICE_FEED = 0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16 # MASTER_ADMIN = 0xd2d03377Fa96687e9C11380DA9956AcC5F307e2c # # Required .env.fork variables: @@ -33,7 +33,7 @@ # POOL_FEE_BPS, DURATION, SPEC_ID, TERMS_HASH, EXPIRATION_TIME # # Live Core addresses (can be overridden via .env.fork for Sepolia or future redeployments): -# STAKE_MANAGER, SLASHING_MANAGER, REWARDS_MANAGER, SSP_ROUTER, CHAINLINK_PRICE_FEED, MASTER_ADMIN +# STAKE_MANAGER, SLASHING_MANAGER, REWARDS_MANAGER, SSP_ROUTER, ORACLE_PRICE_FEED, MASTER_ADMIN # # Covered-vault mode env vars: # MORPHO_VAULT_VERSION (set by Makefile: 1 = MetaMorpho v1, 2 = VaultV2) @@ -81,7 +81,7 @@ STAKE_MANAGER="${STAKE_MANAGER:-0x5be5220F81e76e0CF6089fb7E7aE9eF48a8D64Be}" SLASHING_MANAGER="${SLASHING_MANAGER:-0x7Bc39bf135eF3c30E542C196719c91455ff489f0}" REWARDS_MANAGER="${REWARDS_MANAGER:-0x8ae0F0B94fe782D7F055B04d4699c3cc01632b46}" SSP_ROUTER="${SSP_ROUTER:-0xF39E592E93A7a925a57464e0120B555A25590486}" -CHAINLINK_PRICE_FEED="${CHAINLINK_PRICE_FEED:-0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16}" +ORACLE_PRICE_FEED="${ORACLE_PRICE_FEED:-0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16}" MASTER_ADMIN="${MASTER_ADMIN:-0xd2d03377Fa96687e9C11380DA9956AcC5F307e2c}" # ─── Covered vault mode (always enabled; Makefile sets MORPHO_VAULT_VERSION) ───── @@ -94,7 +94,7 @@ info "STAKE_MANAGER = $STAKE_MANAGER" info "SLASHING_MANAGER = $SLASHING_MANAGER" info "REWARDS_MANAGER = $REWARDS_MANAGER" info "SSP_ROUTER = $SSP_ROUTER" -info "CHAINLINK_PRICE_FEED = $CHAINLINK_PRICE_FEED" +info "ORACLE_PRICE_FEED = $ORACLE_PRICE_FEED" info "MASTER_ADMIN = $MASTER_ADMIN" info "MORPHO_VAULT_VERSION = $MORPHO_VAULT_VERSION" @@ -151,7 +151,7 @@ cast rpc anvil_impersonateAccount "$MASTER_ADMIN" --rpc-url "$ANVIL_RPC_URL" > / cast rpc anvil_setBalance "$MASTER_ADMIN" 0xDE0B6B3A7640000 --rpc-url "$ANVIL_RPC_URL" > /dev/null # DEFAULT_ADMIN_ROLE on all Core contracts -for addr in "$STAKE_MANAGER" "$SLASHING_MANAGER" "$REWARDS_MANAGER" "$SSP_ROUTER" "$CHAINLINK_PRICE_FEED"; do +for addr in "$STAKE_MANAGER" "$SLASHING_MANAGER" "$REWARDS_MANAGER" "$SSP_ROUTER" "$ORACLE_PRICE_FEED"; do cast send --from "$MASTER_ADMIN" --unlocked --rpc-url "$ANVIL_RPC_URL" \ "$addr" "grantRole(bytes32,address)" "$ADMIN_ROLE" "$DEPLOYER" > /dev/null done @@ -166,28 +166,28 @@ cast rpc anvil_stopImpersonatingAccount "$MASTER_ADMIN" --rpc-url "$ANVIL_RPC_UR info "Core roles granted to deployer" # ─── 2b. Register price feeds if missing ────────────────────────────────────── -# WETH/USD is already registered on the live ChainlinkPriceFeed (verified 2026-04). +# WETH/USD is already registered on the live OraclePriceFeed (verified 2026-04). # USDC/USD must be registered because it was not set at deployment time. -step "Registering price feeds on live ChainlinkPriceFeed (if missing)..." +step "Registering price feeds on live OraclePriceFeed (if missing)..." WETH_ADDR="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" USDC_ADDR="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" ETH_USD_AGG="0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" USDC_USD_AGG="0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6" -has_weth=$(cast call "$CHAINLINK_PRICE_FEED" "hasPriceFeed(address)(bool)" "$WETH_ADDR" --rpc-url "$ANVIL_RPC_URL" 2>/dev/null || echo "false") +has_weth=$(cast call "$ORACLE_PRICE_FEED" "hasPriceFeed(address)(bool)" "$WETH_ADDR" --rpc-url "$ANVIL_RPC_URL" 2>/dev/null || echo "false") if [[ "$has_weth" != "true" ]]; then cast send --private-key "$PRIVATE_KEY" --rpc-url "$ANVIL_RPC_URL" \ - "$CHAINLINK_PRICE_FEED" "registerPriceFeed(address,address,uint256)" \ + "$ORACLE_PRICE_FEED" "registerPriceFeed(address,address,uint256)" \ "$WETH_ADDR" "$ETH_USD_AGG" "86400" info "WETH/USD price feed registered" else info "[SKIP] WETH/USD price feed already registered" fi -has_usdc=$(cast call "$CHAINLINK_PRICE_FEED" "hasPriceFeed(address)(bool)" "$USDC_ADDR" --rpc-url "$ANVIL_RPC_URL" 2>/dev/null || echo "false") +has_usdc=$(cast call "$ORACLE_PRICE_FEED" "hasPriceFeed(address)(bool)" "$USDC_ADDR" --rpc-url "$ANVIL_RPC_URL" 2>/dev/null || echo "false") if [[ "$has_usdc" != "true" ]]; then cast send --private-key "$PRIVATE_KEY" --rpc-url "$ANVIL_RPC_URL" \ - "$CHAINLINK_PRICE_FEED" "registerPriceFeed(address,address,uint256)" \ + "$ORACLE_PRICE_FEED" "registerPriceFeed(address,address,uint256)" \ "$USDC_ADDR" "$USDC_USD_AGG" "86400" info "USDC/USD price feed registered" else @@ -206,7 +206,7 @@ NATIVE_WRAPPER="$PAYOUT_TOKEN" \ STAKE_MANAGER="$STAKE_MANAGER" \ SLASHING_MANAGER="$SLASHING_MANAGER" \ REWARDS_MANAGER="$REWARDS_MANAGER" \ -CHAINLINK_PRICE_FEED="$CHAINLINK_PRICE_FEED" \ +ORACLE_PRICE_FEED="$ORACLE_PRICE_FEED" \ forge script --root "$COVERAGE_ROOT" \ "$COVERAGE_ROOT/script/DeployEthereum.s.sol:DeployEthereum" \ --rpc-url "$ANVIL_RPC_URL" \ @@ -432,7 +432,7 @@ CLAIM_MANAGER="$CLAIM_MANAGER" \ PREMIUM_MANAGER="$PREMIUM_MANAGER" \ SPEC_REGISTRY="$SPEC_REGISTRY" \ STAKE_MANAGER="$STAKE_MANAGER" \ -CHAINLINK_PRICE_FEED="$CHAINLINK_PRICE_FEED" \ +ORACLE_PRICE_FEED="$ORACLE_PRICE_FEED" \ POOL_FEE_BPS="${POOL_FEE_BPS:-0}" \ PAYOUT_TOKEN="$PAYOUT_TOKEN" \ DURATION="${DURATION:-15768000}" \ diff --git a/src/ClaimManager.sol b/src/ClaimManager.sol index 434abe11..a9c8c8ab 100644 --- a/src/ClaimManager.sol +++ b/src/ClaimManager.sol @@ -9,7 +9,7 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IChainlinkPriceFeed} from "./interfaces/IChainlinkPriceFeed.sol"; +import {IOraclePriceFeed} from "./interfaces/IOraclePriceFeed.sol"; import {IClaimManager} from "./interfaces/IClaimManager.sol"; import {IPolicyManager} from "./interfaces/IPolicyManager.sol"; import {IPremiumManager} from "./interfaces/IPremiumManager.sol"; @@ -66,7 +66,7 @@ contract ClaimManager is mapping(uint96 policyId => mapping(bytes32 evidenceHash => bool used)) private _usedEvidenceHashes; // --- Storage layout V2 (appended; do not insert variables above this line) --- - address public override chainlinkPriceFeed; + address public override oraclePriceFeed; uint16 public override priceDeviationToleranceBps; // slither-disable-next-line unused-state uint256[48] private __gap; @@ -120,14 +120,14 @@ contract ClaimManager is /** * @inheritdoc IClaimManager */ - function initializeV2(address chainlinkPriceFeed_, uint16 priceDeviationToleranceBps_) external reinitializer(2) { - require(chainlinkPriceFeed_ != address(0), ZeroAddress()); + function initializeV2(address oraclePriceFeed_, uint16 priceDeviationToleranceBps_) external reinitializer(2) { + require(oraclePriceFeed_ != address(0), ZeroAddress()); require( priceDeviationToleranceBps_ <= _MAX_SETTABLE_DEVIATION_BPS, InvalidDeviationBps(priceDeviationToleranceBps_) ); - chainlinkPriceFeed = chainlinkPriceFeed_; + oraclePriceFeed = oraclePriceFeed_; priceDeviationToleranceBps = priceDeviationToleranceBps_; - emit ChainlinkPriceFeedSet(chainlinkPriceFeed_); + emit OraclePriceFeedSet(oraclePriceFeed_); emit PriceDeviationToleranceBpsSet(priceDeviationToleranceBps_); } @@ -202,10 +202,10 @@ contract ClaimManager is /** * @inheritdoc IClaimManager */ - function setChainlinkPriceFeed(address chainlinkPriceFeed_) external override onlyRole(DEFAULT_ADMIN_ROLE) { - require(chainlinkPriceFeed_ != address(0), ZeroAddress()); - chainlinkPriceFeed = chainlinkPriceFeed_; - emit ChainlinkPriceFeedSet(chainlinkPriceFeed_); + function setOraclePriceFeed(address oraclePriceFeed_) external override onlyRole(DEFAULT_ADMIN_ROLE) { + require(oraclePriceFeed_ != address(0), ZeroAddress()); + oraclePriceFeed = oraclePriceFeed_; + emit OraclePriceFeedSet(oraclePriceFeed_); } /** @@ -334,26 +334,18 @@ contract ClaimManager is function _authorizeUpgrade(address) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} /** - * @notice Computes the Chainlink fair value of `amountIn` units of `tokenIn` denominated in `tokenOut`. + * @notice Computes the oracle fair value of `amountIn` units of `tokenIn` denominated in `tokenOut`. * @dev Normalizes the NATIVE_ETH sentinel to the Swapper's nativeWrapper before feed lookups, mirroring * Swapper.quoteSwap so a slashed-native-ETH leg priced against a WETH feed works correctly. - * Both external Chainlink calls are wrapped in try/catch so staleness reverts surface as - * OracleUnavailable rather than propagating raw Chainlink errors. + * Both external oracle calls are wrapped in try/catch so staleness reverts surface as + * OracleUnavailable rather than propagating raw oracle errors. * @param tokenIn Collateral token (may be the NATIVE_ETH sentinel). * @param tokenOut Payout token (may be the NATIVE_ETH sentinel). * @param amountIn Amount of tokenIn to value. - * @return fair Token-native amount of tokenOut equivalent to amountIn of tokenIn at Chainlink prices. + * @return fair Token-native amount of tokenOut equivalent to amountIn of tokenIn at oracle prices. */ - function _chainlinkFairValue( - address tokenIn, - address tokenOut, - uint256 amountIn - ) - private - view - returns (uint256 fair) - { - address feed = chainlinkPriceFeed; + function _oracleFairValue(address tokenIn, address tokenOut, uint256 amountIn) private view returns (uint256 fair) { + address feed = oraclePriceFeed; if (feed == address(0)) revert OracleUnavailable(tokenIn, tokenOut); // Resolve NATIVE_ETH sentinel with a single nativeWrapper() call (at most once per invocation). @@ -367,7 +359,7 @@ contract ClaimManager is if (actualIn == actualOut) return amountIn; - IChainlinkPriceFeed oracle = IChainlinkPriceFeed(feed); + IOraclePriceFeed oracle = IOraclePriceFeed(feed); // slither-disable-next-line calls-loop if (!oracle.hasPriceFeed(actualIn) || !oracle.hasPriceFeed(actualOut)) { @@ -393,27 +385,27 @@ contract ClaimManager is } /** - * @notice Validates that a DEX spot quote is within the configured tolerance of the Chainlink fair value. + * @notice Validates that a DEX spot quote is within the configured tolerance of the oracle fair value. * @dev Reverts with QuoteDeviatesFromOracle when the absolute deviation exceeds priceDeviationToleranceBps. - * Callers must guarantee chainlinkFair > 0 (enforced by _chainlinkFairValue). + * Callers must guarantee oracleFair > 0 (enforced by _oracleFairValue). * @param tokenIn Collateral token (used only in the revert payload). * @param tokenOut Payout token (used only in the revert payload). * @param dexQuote Spot quote returned by the DEX route. - * @param chainlinkFair Chainlink-derived fair value for the same amount. + * @param oracleFair Oracle-derived fair value for the same amount. */ function _validateDexAgainstOracle( address tokenIn, address tokenOut, uint256 dexQuote, - uint256 chainlinkFair + uint256 oracleFair ) private view { - uint256 diff = dexQuote > chainlinkFair ? dexQuote - chainlinkFair : chainlinkFair - dexQuote; - uint256 deviationBps = (diff * 10_000) / chainlinkFair; + uint256 diff = dexQuote > oracleFair ? dexQuote - oracleFair : oracleFair - dexQuote; + uint256 deviationBps = (diff * 10_000) / oracleFair; if (deviationBps > priceDeviationToleranceBps) { - revert QuoteDeviatesFromOracle(tokenIn, tokenOut, dexQuote, chainlinkFair, deviationBps); + revert QuoteDeviatesFromOracle(tokenIn, tokenOut, dexQuote, oracleFair, deviationBps); } } @@ -610,8 +602,8 @@ contract ClaimManager is /** * @notice Computes per-vault token-native slash amounts proportional to each vault's payout-equivalent value. - * @dev Two-pass: values each stake via Chainlink fair value (oracle-anchored), then computes proportional - * slash amounts. The DEX quote is used as a bounded sanity check — if it deviates from the Chainlink + * @dev Two-pass: values each stake via oracle fair value, then computes proportional + * slash amounts. The DEX quote is used as a bounded sanity check — if it deviates from the oracle * fair value by more than `priceDeviationToleranceBps`, the whole claim reverts. * Cross-token slashes are inflated by MAX / (MAX - maxSwapSlippageBps) so the beneficiary receives * the full requestedAmount even at worst-case swap slippage; same-token vaults need no inflation. @@ -645,11 +637,11 @@ contract ClaimManager is if (tokens[i] == payoutToken) { payoutEquivalents[i] = tokenStakes[i]; } else { - // Chainlink fair value is authoritative for slash distribution sizing. + // Oracle fair value is authoritative for slash distribution sizing. // The DEX quote is a bounded sanity check: revert if it deviates too far from fair value, // indicating a manipulated or illiquid pool. // slither-disable-next-line calls-loop - uint256 fair = _chainlinkFairValue(tokens[i], payoutToken, tokenStakes[i]); + uint256 fair = _oracleFairValue(tokens[i], payoutToken, tokenStakes[i]); uint256 dex = 0; // slither-disable-next-line calls-loop @@ -708,7 +700,7 @@ contract ClaimManager is /** * @notice Processes collateral by swapping or transferring directly, capped at remainingPayout. - * @dev amountOutMin is derived from Chainlink fair value with the DEX quote as a bounded sanity check + * @dev amountOutMin is derived from oracle fair value with the DEX quote as a bounded sanity check * (see `_computeAmountOutMin`). Surplus beyond remainingPayout is forwarded via * PremiumManager.distributeSurplusAsRewards() if configured. * vaultIndex is included in the taskId to prevent replay collisions when two vault legs @@ -787,16 +779,16 @@ contract ClaimManager is } /** - * @notice Derives the minimum acceptable swap output, anchored to Chainlink fair value with a bounded + * @notice Derives the minimum acceptable swap output, anchored to oracle fair value with a bounded * DEX sanity check. - * @dev The Chainlink fair value is the primary anchor; the DEX quote is validated against it via + * @dev The oracle fair value is the primary anchor; the DEX quote is validated against it via * `_validateDexAgainstOracle` and then used to determine the tighter of the two bounds: * anchor = min(fair, dex) * amountOutMin = anchor × (MAX − maxSwapSlippageBps) / MAX * Using min(fair, dex) ensures the bound is achievable on-chain when the DEX quote is within - * tolerance below the Chainlink fair value, while a manipulated DEX quote above fair is capped + * tolerance below the oracle fair value, while a manipulated DEX quote above fair is capped * to fair (preventing an inflated bound that cannot be met). - * Reverts with OracleUnavailable if the Chainlink feed is unset, missing, stale, or returns zero. + * Reverts with OracleUnavailable if the oracle feed is unset, missing, stale, or returns zero. * Reverts with QuoteUnavailable if the DEX quote is unavailable or zero. * Reverts with QuoteDeviatesFromOracle if the DEX quote deviates beyond priceDeviationToleranceBps. * Returns at least 1 to prevent a zero-minimum swap that would accept any output. @@ -806,7 +798,7 @@ contract ClaimManager is * @return Slippage-adjusted minimum output amount (≥ 1). */ function _computeAmountOutMin(address tokenIn, address tokenOut, uint256 amountIn) private returns (uint256) { - uint256 fair = _chainlinkFairValue(tokenIn, tokenOut, amountIn); + uint256 fair = _oracleFairValue(tokenIn, tokenOut, amountIn); uint256 dex = 0; try ISwapper(swapper).quoteSwap(tokenIn, tokenOut, amountIn) returns (uint256 quoted) { diff --git a/src/PolicyManager.sol b/src/PolicyManager.sol index c89b9655..4e080901 100644 --- a/src/PolicyManager.sol +++ b/src/PolicyManager.sol @@ -11,7 +11,7 @@ import {ICoverPool} from "./interfaces/ICoverPool.sol"; import {ICoverPoolFactory} from "./interfaces/ICoverPoolFactory.sol"; import {IStakeManager} from "./interfaces/IStakeManager.sol"; import {IBindPolicyHook} from "./interfaces/IBindPolicyHook.sol"; -import {IChainlinkPriceFeed} from "./interfaces/IChainlinkPriceFeed.sol"; +import {IOraclePriceFeed} from "./interfaces/IOraclePriceFeed.sol"; /** * @title PolicyManager @@ -39,7 +39,7 @@ contract PolicyManager is mapping(uint96 policyId => IPolicyManager.PolicyMetadata) private _policies; mapping(bytes32 policyCommit => bool registered) private _registeredPolicyCommits; mapping(address token => bool supported) public override supportedPayoutTokens; - address public override chainlinkPriceFeed; + address public override oraclePriceFeed; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -53,7 +53,7 @@ contract PolicyManager is * @param stakeManager_ Address of the StakeManager contract. * @param claimManager_ Address of the ClaimManager contract. * @param coverPoolFactory_ Address of the CoverPoolFactory contract. - * @param chainlinkPriceFeed_ Address of the ChainlinkPriceFeed contract used for coverageLimit USD conversion. + * @param oraclePriceFeed_ Address of the OraclePriceFeed contract used for coverageLimit USD conversion. * @param initialPayoutTokens_ Payout tokens marked as supported on deployment. */ function initialize( @@ -61,7 +61,7 @@ contract PolicyManager is address stakeManager_, address claimManager_, address coverPoolFactory_, - address chainlinkPriceFeed_, + address oraclePriceFeed_, address[] calldata initialPayoutTokens_ ) external @@ -69,7 +69,7 @@ contract PolicyManager is { require( admin_ != address(0) && stakeManager_ != address(0) && claimManager_ != address(0) - && coverPoolFactory_ != address(0) && chainlinkPriceFeed_ != address(0), + && coverPoolFactory_ != address(0) && oraclePriceFeed_ != address(0), ZeroAddress() ); @@ -81,7 +81,7 @@ contract PolicyManager is stakeManager = stakeManager_; claimManager = claimManager_; coverPoolFactory = coverPoolFactory_; - chainlinkPriceFeed = chainlinkPriceFeed_; + oraclePriceFeed = oraclePriceFeed_; uint256 tokensLength = initialPayoutTokens_.length; for (uint256 i = 0; i < tokensLength; ++i) { @@ -135,10 +135,10 @@ contract PolicyManager is /** * @inheritdoc IPolicyManager */ - function setChainlinkPriceFeed(address chainlinkPriceFeed_) external override onlyRole(DEFAULT_ADMIN_ROLE) { - require(chainlinkPriceFeed_ != address(0), ZeroAddress()); - chainlinkPriceFeed = chainlinkPriceFeed_; - emit ChainlinkPriceFeedSet(chainlinkPriceFeed_); + function setOraclePriceFeed(address oraclePriceFeed_) external override onlyRole(DEFAULT_ADMIN_ROLE) { + require(oraclePriceFeed_ != address(0), ZeroAddress()); + oraclePriceFeed = oraclePriceFeed_; + emit OraclePriceFeedSet(oraclePriceFeed_); } /** @@ -262,11 +262,11 @@ contract PolicyManager is for (uint256 i = 0; i < tokens.length; ++i) { if (tokenStakes[i] > 0) { // slither-disable-next-line calls-loop - totalStakeUSD += IChainlinkPriceFeed(chainlinkPriceFeed).getUSDValue(tokens[i], tokenStakes[i]); + totalStakeUSD += IOraclePriceFeed(oraclePriceFeed).getUSDValue(tokens[i], tokenStakes[i]); } } uint256 coverageLimitUSD = - IChainlinkPriceFeed(chainlinkPriceFeed).getUSDValue(request.payoutToken, boundPolicy.coverageLimit); + IOraclePriceFeed(oraclePriceFeed).getUSDValue(request.payoutToken, boundPolicy.coverageLimit); require(coverageLimitUSD > 0, ZeroCoverageLimitUSD()); require(totalStakeUSD >= coverageLimitUSD, InsufficientStake(policyId, coverageLimitUSD, totalStakeUSD)); diff --git a/src/interfaces/IClaimManager.sol b/src/interfaces/IClaimManager.sol index 153aedcd..9b91888f 100644 --- a/src/interfaces/IClaimManager.sol +++ b/src/interfaces/IClaimManager.sol @@ -150,10 +150,10 @@ interface IClaimManager is IAccessControl { event PremiumManagerSet(address indexed premiumManager); /** - * @notice Emitted when the Chainlink price feed address is updated. - * @param chainlinkPriceFeed New ChainlinkPriceFeed address. + * @notice Emitted when the oracle price feed address is updated. + * @param oraclePriceFeed New OraclePriceFeed address. */ - event ChainlinkPriceFeedSet(address indexed chainlinkPriceFeed); + event OraclePriceFeedSet(address indexed oraclePriceFeed); /** * @notice Emitted when the price deviation tolerance is updated. @@ -207,7 +207,7 @@ interface IClaimManager is IAccessControl { event TokensRecovered(address indexed token, address indexed to, uint256 amount); /** - * @notice Emitted when a cross-token vault is excluded from slashing because the Chainlink-anchored + * @notice Emitted when a cross-token vault is excluded from slashing because the oracle-anchored * proportional slash amount rounds to zero after slippage inflation (dust vault). * @dev Fires only from the proportional-rounding branch — not from an unavailable quote or oracle * (those now revert with `QuoteUnavailable` or `OracleUnavailable` respectively). @@ -344,8 +344,8 @@ interface IClaimManager is IAccessControl { error QuoteUnavailable(address collateralToken, address payoutToken); /** - * @notice Reverts when a Chainlink fair value cannot be computed for a cross-token collateral leg. - * This occurs when: the ClaimManager's chainlinkPriceFeed is unset, a feed is missing for + * @notice Reverts when a fair value cannot be computed for a cross-token collateral leg. + * This occurs when: the ClaimManager's oraclePriceFeed is unset, a feed is missing for * one of the tokens, the feed data is stale, or the USD/token conversion yields zero. * @param tokenIn Collateral token address (or NATIVE_ETH sentinel). * @param tokenOut Payout token address (or NATIVE_ETH sentinel). @@ -353,16 +353,16 @@ interface IClaimManager is IAccessControl { error OracleUnavailable(address tokenIn, address tokenOut); /** - * @notice Reverts when the DEX spot quote deviates from the Chainlink fair value by more than + * @notice Reverts when the DEX spot quote deviates from the oracle fair value by more than * the configured `priceDeviationToleranceBps`, indicating a manipulated or thin pool. * @param tokenIn Collateral token address. * @param tokenOut Payout token address. * @param dexQuote Spot quote returned by the DEX route. - * @param chainlinkFair Chainlink-derived fair value for the same amount. + * @param oracleFair Oracle-derived fair value for the same amount. * @param deviationBps Actual deviation in basis points. */ error QuoteDeviatesFromOracle( - address tokenIn, address tokenOut, uint256 dexQuote, uint256 chainlinkFair, uint256 deviationBps + address tokenIn, address tokenOut, uint256 dexQuote, uint256 oracleFair, uint256 deviationBps ); /** @@ -552,33 +552,33 @@ interface IClaimManager is IAccessControl { * @notice Initializes V2 state atomically during a UUPS upgrade via upgradeToAndCall. * @dev Uses OpenZeppelin's `reinitializer(2)` pattern. Must be called exactly once, embedded in the * upgradeToAndCall calldata. For fresh deployments, call immediately after `initialize`. - * @param chainlinkPriceFeed_ Address of the ChainlinkPriceFeed contract. - * @param priceDeviationToleranceBps_ Maximum allowed deviation between DEX quote and Chainlink fair + * @param oraclePriceFeed_ Address of the OraclePriceFeed contract. + * @param priceDeviationToleranceBps_ Maximum allowed deviation between DEX quote and oracle fair * value, in basis points. Must be <= 1000 (10%). */ - function initializeV2(address chainlinkPriceFeed_, uint16 priceDeviationToleranceBps_) external; + function initializeV2(address oraclePriceFeed_, uint16 priceDeviationToleranceBps_) external; /** - * @notice Sets the Chainlink price feed address used for cross-token claim valuation. + * @notice Sets the oracle price feed address used for cross-token claim valuation. * @dev Requires DEFAULT_ADMIN_ROLE. - * @param chainlinkPriceFeed_ Address of the ChainlinkPriceFeed contract. Must be non-zero. + * @param oraclePriceFeed_ Address of the OraclePriceFeed contract. Must be non-zero. */ - function setChainlinkPriceFeed(address chainlinkPriceFeed_) external; + function setOraclePriceFeed(address oraclePriceFeed_) external; /** - * @notice Sets the maximum allowed deviation between DEX quote and Chainlink fair value. + * @notice Sets the maximum allowed deviation between DEX quote and oracle fair value. * @dev Requires DEFAULT_ADMIN_ROLE. Must be <= 1000 bps (10%). * @param priceDeviationToleranceBps_ Tolerance in basis points (e.g. 300 = 3%). */ function setPriceDeviationToleranceBps(uint16 priceDeviationToleranceBps_) external; /** - * @notice Returns the Chainlink price feed address used for cross-token claim valuation. + * @notice Returns the oracle price feed address used for cross-token claim valuation. */ - function chainlinkPriceFeed() external view returns (address); + function oraclePriceFeed() external view returns (address); /** - * @notice Returns the maximum allowed deviation between DEX spot quote and Chainlink fair value, + * @notice Returns the maximum allowed deviation between DEX spot quote and oracle fair value, * in basis points. Cross-token claims revert with `QuoteDeviatesFromOracle` if exceeded. */ function priceDeviationToleranceBps() external view returns (uint16); diff --git a/src/interfaces/IChainlinkPriceFeed.sol b/src/interfaces/IOraclePriceFeed.sol similarity index 78% rename from src/interfaces/IChainlinkPriceFeed.sol rename to src/interfaces/IOraclePriceFeed.sol index cb36e915..2cc232e6 100644 --- a/src/interfaces/IChainlinkPriceFeed.sol +++ b/src/interfaces/IOraclePriceFeed.sol @@ -2,10 +2,10 @@ pragma solidity 0.8.28; /** - * @title IChainlinkPriceFeed - * @notice Interface for Catalysis Core's ChainlinkPriceFeed that converts token amounts to USD values. + * @title IOraclePriceFeed + * @notice Interface for Catalysis Core's OraclePriceFeed that converts token amounts to USD values. */ -interface IChainlinkPriceFeed { +interface IOraclePriceFeed { /** * @notice Returns the USD value of a token amount. * @param token ERC20 token address. @@ -23,7 +23,7 @@ interface IChainlinkPriceFeed { function getTokenAmount(address token, uint256 amountUSD) external view returns (uint256); /** - * @notice Returns whether a Chainlink price feed is registered for the given token. + * @notice Returns whether a price feed is registered for the given token. * @param token ERC20 token address. * @return True if a price feed exists for the token. */ diff --git a/src/interfaces/IPolicyManager.sol b/src/interfaces/IPolicyManager.sol index 8b7dd697..d6fe397c 100644 --- a/src/interfaces/IPolicyManager.sol +++ b/src/interfaces/IPolicyManager.sol @@ -176,10 +176,10 @@ interface IPolicyManager is IAccessControl { event PayoutTokenSet(address indexed token, bool supported); /** - * @notice Emitted when the ChainlinkPriceFeed dependency is set or updated. - * @param chainlinkPriceFeed_ Address of the Chainlink price feed contract. + * @notice Emitted when the OraclePriceFeed dependency is set or updated. + * @param oraclePriceFeed_ Address of the oracle price feed contract. */ - event ChainlinkPriceFeedSet(address indexed chainlinkPriceFeed_); + event OraclePriceFeedSet(address indexed oraclePriceFeed_); /** * @notice Emitted when a policy's premium default status is updated by the pool curator. @@ -333,11 +333,11 @@ interface IPolicyManager is IAccessControl { function setCoverPoolFactory(address coverPoolFactory_) external; /** - * @notice Sets the ChainlinkPriceFeed contract used for stake validation at bind time. - * @dev Requires DEFAULT_ADMIN_ROLE. Emits ChainlinkPriceFeedSet. - * @param chainlinkPriceFeed_ Address of the ChainlinkPriceFeed implementation. + * @notice Sets the OraclePriceFeed contract used for stake validation at bind time. + * @dev Requires DEFAULT_ADMIN_ROLE. Emits OraclePriceFeedSet. + * @param oraclePriceFeed_ Address of the OraclePriceFeed implementation. */ - function setChainlinkPriceFeed(address chainlinkPriceFeed_) external; + function setOraclePriceFeed(address oraclePriceFeed_) external; /** * @notice Requests coverage from a specified pool (user-facing entrypoint). @@ -411,9 +411,9 @@ interface IPolicyManager is IAccessControl { function coverPoolFactory() external view returns (address); /** - * @notice Returns the configured ChainlinkPriceFeed address. + * @notice Returns the configured OraclePriceFeed address. */ - function chainlinkPriceFeed() external view returns (address); + function oraclePriceFeed() external view returns (address); /** * @notice Adds or removes a payout token from the supported whitelist. diff --git a/test/defi/CoveredVaultWrapper.fork.t.sol b/test/defi/CoveredVaultWrapper.fork.t.sol index 5ddf8d9e..02b85f47 100644 --- a/test/defi/CoveredVaultWrapper.fork.t.sol +++ b/test/defi/CoveredVaultWrapper.fork.t.sol @@ -49,9 +49,9 @@ contract CoveredVaultWrapperForkTest is ForkCoverageSetup { _advancePolicyId(); _wireCoverageToLiveCore(); - // USDC feed is not registered on live ChainlinkPriceFeed at fork block; WETH is. - _registerChainlinkFeedIfMissing(WETH, ETH_USD_AGGREGATOR, 86_400); - _registerChainlinkFeedIfMissing(USDC, USDC_USD_AGGREGATOR, 86_400); + // USDC feed is not registered on live OraclePriceFeed at fork block; WETH is. + _registerPriceFeedIfMissing(WETH, ETH_USD_AGGREGATOR, 86_400); + _registerPriceFeedIfMissing(USDC, USDC_USD_AGGREGATOR, 86_400); _whitelistPayoutToken(USDC); _deploySpec(); @@ -83,7 +83,7 @@ contract CoveredVaultWrapperForkTest is ForkCoverageSetup { coverPool, address(wrapper), address(wrapper), USDC, 2 * 365 days, address(wrapper) ); - // Fund EigenLayer stake before binding (binding checks stake >= coverageLimit via ChainlinkPriceFeed). + // Fund EigenLayer stake before binding (binding checks stake >= coverageLimit via OraclePriceFeed). address[] memory vaults = _discoverNewEigenLayerVaults(vaultCountBefore); require(vaults.length > 0, "no new EigenLayer vaults deployed for committee"); for (uint256 i = 0; i < vaults.length; i++) { diff --git a/test/defi/helpers/ForkCoverageSetup.sol b/test/defi/helpers/ForkCoverageSetup.sol index c19f9f04..5eda1de6 100644 --- a/test/defi/helpers/ForkCoverageSetup.sol +++ b/test/defi/helpers/ForkCoverageSetup.sol @@ -47,7 +47,7 @@ interface ISSPRouterFork { function getCommitteeVaultsByModule(uint96 committeeId, uint8 moduleType) external view returns (address[] memory); } -interface IChainlinkPriceFeedFork { +interface IOraclePriceFeedFork { function hasPriceFeed(address token) external view returns (bool); function registerPriceFeed(address token, address aggregator, uint256 stalenessThreshold) external; function getUSDValue(address token, uint256 amount) external view returns (uint256); @@ -93,7 +93,7 @@ abstract contract ForkCoverageSetup is Test { address internal constant SLASHING_MANAGER = 0x7Bc39bf135eF3c30E542C196719c91455ff489f0; address internal constant REWARDS_MANAGER = 0x8ae0F0B94fe782D7F055B04d4699c3cc01632b46; address internal constant SSP_ROUTER = 0xF39E592E93A7a925a57464e0120B555A25590486; - address internal constant CHAINLINK_PRICEFEED = 0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16; + address internal constant ORACLE_PRICEFEED = 0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16; address internal constant MASTER_ADMIN = 0xd2d03377Fa96687e9C11380DA9956AcC5F307e2c; /// @dev EigenLayer @@ -202,7 +202,7 @@ abstract contract ForkCoverageSetup is Test { address(pmImplContract), abi.encodeCall( PolicyManager.initialize, - (deployer, STAKE_MANAGER, deployer, deployer, CHAINLINK_PRICEFEED, new address[](0)) + (deployer, STAKE_MANAGER, deployer, deployer, ORACLE_PRICEFEED, new address[](0)) ) ) ) @@ -297,12 +297,12 @@ abstract contract ForkCoverageSetup is Test { premiumManager.approveSpender(WETH, REWARDS_MANAGER, type(uint256).max); } - /// @notice Idempotent: registers a Chainlink price feed on the live ChainlinkPriceFeed if missing. - function _registerChainlinkFeedIfMissing(address token, address aggregator, uint256 staleness) internal { - if (!IChainlinkPriceFeedFork(CHAINLINK_PRICEFEED).hasPriceFeed(token)) { + /// @notice Idempotent: registers a price feed on the live OraclePriceFeed if missing. + function _registerPriceFeedIfMissing(address token, address aggregator, uint256 staleness) internal { + if (!IOraclePriceFeedFork(ORACLE_PRICEFEED).hasPriceFeed(token)) { vm.deal(MASTER_ADMIN, address(MASTER_ADMIN).balance + 1 ether); vm.startPrank(MASTER_ADMIN); - IChainlinkPriceFeedFork(CHAINLINK_PRICEFEED).registerPriceFeed(token, aggregator, staleness); + IOraclePriceFeedFork(ORACLE_PRICEFEED).registerPriceFeed(token, aggregator, staleness); vm.stopPrank(); } } @@ -428,7 +428,7 @@ abstract contract ForkCoverageSetup is Test { view returns (uint256 collateralAmount) { - IChainlinkPriceFeedFork feed = IChainlinkPriceFeedFork(CHAINLINK_PRICEFEED); + IOraclePriceFeedFork feed = IOraclePriceFeedFork(ORACLE_PRICEFEED); uint256 targetUSD = feed.getUSDValue(USDC, usdcAmount) * 2; uint256 tokenDecimals = 10 ** IERC20Metadata(collateralToken).decimals(); uint256 tokenPriceUSD = feed.getUSDValue(collateralToken, tokenDecimals); diff --git a/test/defi/helpers/RealCoverageSetup.sol b/test/defi/helpers/RealCoverageSetup.sol index 75823882..19b99243 100644 --- a/test/defi/helpers/RealCoverageSetup.sol +++ b/test/defi/helpers/RealCoverageSetup.sol @@ -16,7 +16,7 @@ import {EfficientHashLib} from "@solady/utils/EfficientHashLib.sol"; import {MockClaimManager} from "../mocks/Mocks.sol"; import { MockStakeManager, - MockChainlinkPriceFeedDefi, + MockOraclePriceFeedDefi, MockCoverPoolFactory, MockSpecRegistry, MockBindPolicyHook, @@ -27,7 +27,7 @@ import { /// @notice Abstract base contract that deploys real PolicyManager + real CoverPool for adapter tests /// @dev Replaces MockPolicyManager and MockCoverPool with real implementations. /// MockClaimManager is retained because the real ClaimManager.fileClaim() requires deep dependencies -/// (ChainlinkPriceFeed, SlashingManager, Swapper) that are external to the coverage protocol. +/// (OraclePriceFeed, SlashingManager, Swapper) that are external to the coverage protocol. /// Peripheral mocks (StakeManager, CoverPoolFactory, SpecRegistry) are lightweight shims. abstract contract RealCoverageSetup is Test { // ── Real contracts ── @@ -40,7 +40,7 @@ abstract contract RealCoverageSetup is Test { MockCoverPoolFactory internal mockCoverPoolFactory; MockSpecRegistry internal mockSpecRegistry; MockBindPolicyHook internal mockBindPolicyHook; - MockChainlinkPriceFeedDefi internal mockChainlinkPriceFeed; + MockOraclePriceFeedDefi internal mockOraclePriceFeed; MockPremiumManager internal mockPremiumManager; // ── EIP-712 quote signing ── @@ -70,14 +70,14 @@ abstract contract RealCoverageSetup is Test { quoteSigner = vm.addr(quoteSignerKey); // ── Deploy MockClaimManager with pre-computed PolicyManager proxy address ── - // Nonce layout: +0 = MockClaimManager, +1 = MockChainlinkPriceFeedDefi, +2 = PolicyManager impl, + // Nonce layout: +0 = MockClaimManager, +1 = MockOraclePriceFeedDefi, +2 = PolicyManager impl, // +3 = ERC1967Proxy (= PM proxy) uint256 nonce = vm.getNonce(address(this)); address expectedPmProxy = vm.computeCreateAddress(address(this), nonce + 3); claimManager = new MockClaimManager(expectedPmProxy); - // ── Deploy MockChainlinkPriceFeedDefi (returns input amount as USD value) ── - mockChainlinkPriceFeed = new MockChainlinkPriceFeedDefi(); + // ── Deploy MockOraclePriceFeedDefi (returns input amount as USD value) ── + mockOraclePriceFeed = new MockOraclePriceFeedDefi(); // ── Deploy real PolicyManager behind UUPS proxy ── PolicyManager pmImpl = new PolicyManager(); @@ -92,7 +92,7 @@ abstract contract RealCoverageSetup is Test { address(mockStakeManager), address(claimManager), address(mockCoverPoolFactory), - address(mockChainlinkPriceFeed), + address(mockOraclePriceFeed), new address[](0) ) ) diff --git a/test/defi/mocks/CoverageMocks.sol b/test/defi/mocks/CoverageMocks.sol index 6f7bd17b..0f77f372 100644 --- a/test/defi/mocks/CoverageMocks.sol +++ b/test/defi/mocks/CoverageMocks.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.28; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IChainlinkPriceFeed} from "coverage/interfaces/IChainlinkPriceFeed.sol"; +import {IOraclePriceFeed} from "coverage/interfaces/IOraclePriceFeed.sol"; import {ISpec} from "coverage/interfaces/ISpec.sol"; import {IBindPolicyHook} from "coverage/interfaces/IBindPolicyHook.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; @@ -63,9 +63,9 @@ contract MockStakeManager { } } -/// @title MockChainlinkPriceFeedDefi +/// @title MockOraclePriceFeedDefi /// @notice Returns input amount as USD value (1:1 identity) so stake checks always pass when stakes >= coverageLimit -contract MockChainlinkPriceFeedDefi is IChainlinkPriceFeed { +contract MockOraclePriceFeedDefi is IOraclePriceFeed { function getUSDValue(address, uint256 amount) external pure override returns (uint256) { return amount; } diff --git a/test/fuzz/ClaimManager.t.sol b/test/fuzz/ClaimManager.t.sol index ab8d82de..df7fe48a 100644 --- a/test/fuzz/ClaimManager.t.sol +++ b/test/fuzz/ClaimManager.t.sol @@ -122,8 +122,8 @@ contract FuzzSwapper { } } -/// @dev 1:1 Chainlink feed mock for fuzz tests. All tokens have a feed; getUSDValue and getTokenAmount are 1:1. -contract FuzzChainlinkPriceFeed { +/// @dev 1:1 oracle feed mock for fuzz tests. All tokens have a feed; getUSDValue and getTokenAmount are 1:1. +contract FuzzOraclePriceFeed { function hasPriceFeed(address) external pure returns (bool) { return true; } @@ -175,7 +175,7 @@ contract ClaimManagerFuzzTest is Test { FuzzCoverPool internal pool; FuzzToken internal collateralToken; FuzzToken internal payoutToken; - FuzzChainlinkPriceFeed internal chainlinkFeed; + FuzzOraclePriceFeed internal oracleFeed; address internal admin = makeAddr("admin"); address internal claimer = makeAddr("claimer"); @@ -195,7 +195,7 @@ contract ClaimManagerFuzzTest is Test { pool = new FuzzCoverPool(operator); collateralToken = new FuzzToken("Collateral", "COL"); payoutToken = new FuzzToken("Payout", "PAY"); - chainlinkFeed = new FuzzChainlinkPriceFeed(); + oracleFeed = new FuzzOraclePriceFeed(); ClaimManager impl = new ClaimManager(); address proxy = address( @@ -208,7 +208,7 @@ contract ClaimManagerFuzzTest is Test { ) ); claimManager = ClaimManager(payable(proxy)); - claimManager.initializeV2(address(chainlinkFeed), 300); + claimManager.initializeV2(address(oracleFeed), 300); vm.warp(START); _setupPolicy(COVERAGE_LIMIT()); diff --git a/test/unit/ClaimManager.t.sol b/test/unit/ClaimManager.t.sol index ff90d6e6..a7460a02 100644 --- a/test/unit/ClaimManager.t.sol +++ b/test/unit/ClaimManager.t.sol @@ -47,7 +47,7 @@ contract ClaimManagerTest is Test { MockToken internal collateralTokenMock; MockToken internal payoutTokenMock; MockCoverPool internal mockPool; - MockChainlinkPriceFeed internal chainlinkFeedMock; + MockOraclePriceFeed internal oracleFeedMock; bytes32 internal evidenceHash; bytes32 internal policyCommit; @@ -85,7 +85,7 @@ contract ClaimManagerTest is Test { event TokensRecovered(address indexed token, address indexed to, uint256 amount); event VaultExcludedFromSlashing(address indexed vault, address indexed token); event PremiumManagerSet(address indexed premiumManager); - event ChainlinkPriceFeedSet(address indexed chainlinkPriceFeed); + event OraclePriceFeedSet(address indexed oraclePriceFeed); event PriceDeviationToleranceBpsSet(uint16 priceDeviationToleranceBps); event SwapSurplusDistributed( uint96 indexed policyId, uint256 indexed claimId, address indexed payoutToken, uint256 surplusAmount @@ -144,7 +144,7 @@ contract ClaimManagerTest is Test { vm.label(collateralToken, "CollateralToken"); vm.label(payoutToken, "PayoutToken"); - chainlinkFeedMock = new MockChainlinkPriceFeed(); + oracleFeedMock = new MockOraclePriceFeed(); Options memory deployOpts; deployOpts.unsafeSkipAllChecks = true; @@ -156,10 +156,10 @@ contract ClaimManagerTest is Test { claimManager = ClaimManager(payable(proxy)); vm.prank(admin); - claimManager.initializeV2(address(chainlinkFeedMock), 300); + claimManager.initializeV2(address(oracleFeedMock), 300); vm.label(address(claimManager), "ClaimManager"); - vm.label(address(chainlinkFeedMock), "ChainlinkPriceFeed"); + vm.label(address(oracleFeedMock), "OraclePriceFeed"); _setupDefaultPolicy(); } @@ -223,7 +223,7 @@ contract ClaimManagerTest is Test { INITIALIZE V2 //////////////////////////////////////////////////////////////*/ - function test_initializeV2_WhenZeroChainlink_Reverts() public { + function test_initializeV2_WhenZeroOraclePriceFeed_Reverts() public { ClaimManager impl = new ClaimManager(); ClaimManager cm = ClaimManager(payable(address(new ERC1967Proxy(address(impl), "")))); cm.initialize(admin, specRegistry, slashingManager, swapper, policy); @@ -236,12 +236,12 @@ contract ClaimManagerTest is Test { ClaimManager cm = ClaimManager(payable(address(new ERC1967Proxy(address(impl), "")))); cm.initialize(admin, specRegistry, slashingManager, swapper, policy); vm.expectRevert(abi.encodeWithSelector(IClaimManager.InvalidDeviationBps.selector, uint16(1001))); - cm.initializeV2(address(chainlinkFeedMock), 1001); + cm.initializeV2(address(oracleFeedMock), 1001); } function test_initializeV2_WhenCalledTwice_Reverts() public { vm.expectRevert(); - claimManager.initializeV2(address(chainlinkFeedMock), 300); + claimManager.initializeV2(address(oracleFeedMock), 300); } function test_initializeV2_EmitsBothSetterEvents() public { @@ -250,23 +250,23 @@ contract ClaimManagerTest is Test { cm.initialize(admin, specRegistry, slashingManager, swapper, policy); vm.expectEmit(true, false, false, false, address(cm)); - emit ChainlinkPriceFeedSet(address(chainlinkFeedMock)); + emit OraclePriceFeedSet(address(oracleFeedMock)); vm.expectEmit(false, false, false, true, address(cm)); emit PriceDeviationToleranceBpsSet(300); - cm.initializeV2(address(chainlinkFeedMock), 300); + cm.initializeV2(address(oracleFeedMock), 300); } function test_initializeV2_PopulatesStateCorrectly() public view { - assertEq(claimManager.chainlinkPriceFeed(), address(chainlinkFeedMock)); + assertEq(claimManager.oraclePriceFeed(), address(oracleFeedMock)); assertEq(claimManager.priceDeviationToleranceBps(), 300); } /*////////////////////////////////////////////////////////////// - SET CHAINLINK PRICE FEED + SET ORACLE PRICE FEED //////////////////////////////////////////////////////////////*/ - function test_setChainlinkPriceFeed_WhenCallerNotAdmin_Reverts() public { + function test_setOraclePriceFeed_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( abi.encodeWithSelector( IAccessControl.AccessControlUnauthorizedAccount.selector, @@ -275,22 +275,22 @@ contract ClaimManagerTest is Test { ) ); vm.prank(unauthorized); - claimManager.setChainlinkPriceFeed(address(chainlinkFeedMock)); + claimManager.setOraclePriceFeed(address(oracleFeedMock)); } - function test_setChainlinkPriceFeed_WhenZero_Reverts() public { + function test_setOraclePriceFeed_WhenZero_Reverts() public { vm.expectRevert(IClaimManager.ZeroAddress.selector); vm.prank(admin); - claimManager.setChainlinkPriceFeed(address(0)); + claimManager.setOraclePriceFeed(address(0)); } - function test_setChainlinkPriceFeed_EmitsEvent() public { + function test_setOraclePriceFeed_EmitsEvent() public { address newFeed = makeAddr("newFeed"); vm.expectEmit(true, false, false, false, address(claimManager)); - emit ChainlinkPriceFeedSet(newFeed); + emit OraclePriceFeedSet(newFeed); vm.prank(admin); - claimManager.setChainlinkPriceFeed(newFeed); - assertEq(claimManager.chainlinkPriceFeed(), newFeed); + claimManager.setOraclePriceFeed(newFeed); + assertEq(claimManager.oraclePriceFeed(), newFeed); } /*////////////////////////////////////////////////////////////// @@ -811,8 +811,8 @@ contract ClaimManagerTest is Test { FILE CLAIM — ORACLE PROTECTION (CYS3-03) //////////////////////////////////////////////////////////////*/ - function test_fileClaim_WhenChainlinkPriceFeedUnset_RevertsOracleUnavailable() public { - // Deploy a fresh ClaimManager that was never given initializeV2 (chainlinkPriceFeed == address(0)) + function test_fileClaim_WhenOraclePriceFeedUnset_RevertsOracleUnavailable() public { + // Deploy a fresh ClaimManager that was never given initializeV2 (oraclePriceFeed == address(0)) Options memory opts; opts.unsafeSkipAllChecks = true; address freshProxy = Upgrades.deployUUPSProxy( @@ -838,8 +838,8 @@ contract ClaimManagerTest is Test { freshCm.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); } - function test_fileClaim_WhenChainlinkFeedMissingForCollateral_RevertsOracleUnavailable() public { - chainlinkFeedMock.setFeedMissing(collateralToken); + function test_fileClaim_WhenOracleFeedMissingForCollateral_RevertsOracleUnavailable() public { + oracleFeedMock.setFeedMissing(collateralToken); specMock.setEvaluationResult(true, keccak256("approved")); address[] memory assets = new address[](1); @@ -853,8 +853,8 @@ contract ClaimManagerTest is Test { claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); } - function test_fileClaim_WhenChainlinkFeedMissingForPayoutToken_RevertsOracleUnavailable() public { - chainlinkFeedMock.setFeedMissing(payoutToken); + function test_fileClaim_WhenOracleFeedMissingForPayoutToken_RevertsOracleUnavailable() public { + oracleFeedMock.setFeedMissing(payoutToken); specMock.setEvaluationResult(true, keccak256("approved")); address[] memory assets = new address[](1); @@ -868,8 +868,8 @@ contract ClaimManagerTest is Test { claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); } - function test_fileClaim_WhenChainlinkPriceStale_RevertsOracleUnavailable() public { - chainlinkFeedMock.setShouldRevert(true); + function test_fileClaim_WhenOraclePriceStale_RevertsOracleUnavailable() public { + oracleFeedMock.setShouldRevert(true); specMock.setEvaluationResult(true, keccak256("approved")); address[] memory assets = new address[](1); @@ -885,7 +885,7 @@ contract ClaimManagerTest is Test { function test_fileClaim_WhenDexQuoteDeviatesAboveTolerance_RevertsQuoteDeviatesFromOracle() public { // Configure a swapper where quoteSwap for collateralToken returns 5000 ether - // but Chainlink fair value is 1:1 = SLASH_AMOUNT. + // but oracle fair value is 1:1 = SLASH_AMOUNT. // Deviation = (5000 ether - 500 ether) / 500 ether * 10000 = 90000 bps >> 300 bps tolerance. MockMultiSwapper deviatingSwapper = new MockMultiSwapper(); deviatingSwapper.setQuoteOutput(collateralToken, 5000 ether); // quoteSwap returns 5000 ether @@ -901,7 +901,7 @@ contract ClaimManagerTest is Test { amounts[0] = SLASH_AMOUNT; slashingManagerMock.setSlashResult(assets, amounts); - // chainlinkFair = SLASH_AMOUNT (1:1); dex = 5000 ether → huge upward deviation + // oracleFair = SLASH_AMOUNT (1:1); dex = 5000 ether → huge upward deviation vm.expectRevert( abi.encodeWithSelector( IClaimManager.QuoteDeviatesFromOracle.selector, @@ -918,7 +918,7 @@ contract ClaimManagerTest is Test { function test_fileClaim_WhenDexQuoteDeviatesBelowTolerance_RevertsQuoteDeviatesFromOracle() public { // Configure a swapper where quoteSwap for collateralToken returns 400 ether (20% below 500) - // but Chainlink fair value is 1:1 = SLASH_AMOUNT = 500 ether. + // but oracle fair value is 1:1 = SLASH_AMOUNT = 500 ether. // Deviation = (500 - 400) / 500 * 10000 = 2000 bps >> 300 bps tolerance. MockMultiSwapper deviatingSwapper = new MockMultiSwapper(); deviatingSwapper.setQuoteOutput(collateralToken, 400 ether); // quoteSwap returns 400 ether @@ -948,9 +948,9 @@ contract ClaimManagerTest is Test { claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); } - function test_fileClaim_WhenDexQuoteWithinTolerance_SlashSizingUsesChainlinkFair() public { - // DEX quote and Chainlink fair are both 1:1 (deviation = 0 bps → well within 300 bps tolerance). - // Slash sizing uses payoutEquivalents[i] = _chainlinkFairValue(token, payout, stake) = stake. + function test_fileClaim_WhenDexQuoteWithinTolerance_SlashSizingUsesOracleFair() public { + // DEX quote and oracle fair are both 1:1 (deviation = 0 bps → well within 300 bps tolerance). + // Slash sizing uses payoutEquivalents[i] = _oracleFairValue(token, payout, stake) = stake. // The proportional slash is: baseSlash = stake * requested / stake = requested, inflated by slippage. uint256 stakeA = 300 ether; uint256 requestedAmount = 100 ether; @@ -1039,11 +1039,11 @@ contract ClaimManagerTest is Test { assertEq(uint8(claimManager.claimRecord(POLICY_ID, claimId).status), uint8(IClaimManager.ClaimStatus.Approved)); } - function test_fileClaim_NativeEthCollateral_NormalizesToWrapperForChainlink() public { - // When the collateral is NATIVE_ETH, _chainlinkFairValue must query nativeWrapper not NATIVE_ETH. + function test_fileClaim_NativeEthCollateral_NormalizesToWrapperForOracle() public { + // When the collateral is NATIVE_ETH, _oracleFairValue must query nativeWrapper not NATIVE_ETH. // Mark NATIVE_ETH as missing but nativeWrapper as present in the mock feed. address nativeWrapper = swapperMock.nativeWrapper(); - chainlinkFeedMock.setFeedMissing(NATIVE_ETH); // ensures we only pass via nativeWrapper path + oracleFeedMock.setFeedMissing(NATIVE_ETH); // ensures we only pass via nativeWrapper path // Set up a policy where payoutToken == nativeWrapper so same-token path is taken for NATIVE_ETH // (after normalization, NATIVE_ETH → nativeWrapper == payoutToken → same-token path → no oracle needed) @@ -2902,7 +2902,7 @@ contract ClaimManagerTest is Test { assertEq(uint8(record.status), uint8(IClaimManager.ClaimStatus.Approved), "Claim should be approved"); } - function test_fileClaim_WhenDexQuoteDeviatesFromChainlinkByPerTokenDivisor_RevertsQuoteDeviatesFromOracle() public { + function test_fileClaim_WhenDexQuoteDeviatesFromOracleByPerTokenDivisor_RevertsQuoteDeviatesFromOracle() public { (address tinyVaultAddr, address tinyToken) = _setupTinyBaseSlashWithRealisticQuoter(); // Silence unused variable warning tinyVaultAddr; @@ -2910,7 +2910,7 @@ contract ClaimManagerTest is Test { specMock.setEvaluationResult(true, keccak256("approved")); // The MockSwapperWithPerTokenDivisor divides the quote by 1001, so for tinyStake=1001: - // dex = 1001 / 1001 = 1; chainlinkFair = 1001 (1:1 mock). + // dex = 1001 / 1001 = 1; oracleFair = 1001 (1:1 mock). // Deviation = (1000 / 1001) * 10000 ≈ 9990 bps > priceDeviationToleranceBps(300) → revert. vm.expectRevert( abi.encodeWithSelector( @@ -2918,7 +2918,7 @@ contract ClaimManagerTest is Test { tinyToken, payoutToken, uint256(1), // dex quote (1001/1001) - uint256(1001), // chainlink fair (1:1) + uint256(1001), // oracle fair (1:1) uint256(9990) // actual deviationBps ) ); @@ -4085,13 +4085,13 @@ contract MockPolicy { } /** - * @dev Configurable Chainlink price feed mock. + * @dev Configurable oracle price feed mock. * - All tokens default to a 1:1 price (price = 1e18). * - `setFeedMissing(token)` makes hasPriceFeed return false and getUSDValue revert. * - `setShouldRevert(true)` makes every external call revert (staleness simulation). * - `setPrice(token, price)` overrides the per-token price (in 1e18 scale). */ -contract MockChainlinkPriceFeed { +contract MockOraclePriceFeed { mapping(address => uint256) private _pricePerToken; mapping(address => bool) private _feedMissing; bool private _shouldRevert; @@ -4113,8 +4113,8 @@ contract MockChainlinkPriceFeed { } function getUSDValue(address token, uint256 amount) external view returns (uint256) { - require(!_shouldRevert, "MockChainlinkPriceFeed: stale"); - require(!_feedMissing[token], "MockChainlinkPriceFeed: no feed"); + require(!_shouldRevert, "MockOraclePriceFeed: stale"); + require(!_feedMissing[token], "MockOraclePriceFeed: no feed"); uint256 price = _pricePerToken[token]; if (price == 0) { return amount; // 1:1 default @@ -4123,8 +4123,8 @@ contract MockChainlinkPriceFeed { } function getTokenAmount(address token, uint256 usdValue) external view returns (uint256) { - require(!_shouldRevert, "MockChainlinkPriceFeed: stale"); - require(!_feedMissing[token], "MockChainlinkPriceFeed: no feed"); + require(!_shouldRevert, "MockOraclePriceFeed: stale"); + require(!_feedMissing[token], "MockOraclePriceFeed: no feed"); uint256 price = _pricePerToken[token]; if (price == 0) { return usdValue; // 1:1 default @@ -4150,7 +4150,7 @@ contract MockMultiSwapper { _swapOutputs[tokenIn] = output; } - /// @dev Separate quoteSwap amount — allows passing oracle validation (1:1 with chainlink) + /// @dev Separate quoteSwap amount — allows passing oracle validation (1:1 with oracle) /// while executeSwap returns a different amount (simulating price impact). function setQuoteOutput(address tokenIn, uint256 output) external { _quoteOutputs[tokenIn] = output; @@ -4165,7 +4165,7 @@ contract MockMultiSwapper { function quoteSwap(address tokenIn, address, uint256 amountIn) external view returns (uint256) { uint256 q = _quoteOutputs[tokenIn]; if (q > 0) return q; - return amountIn; // 1:1 default: consistent with Chainlink 1:1 mock + return amountIn; // 1:1 default: consistent with oracle 1:1 mock } function executeSwap(ISwapper.SwapParams calldata params) external returns (uint256) { diff --git a/test/unit/PolicyManager.t.sol b/test/unit/PolicyManager.t.sol index e7d06e05..85b3caeb 100644 --- a/test/unit/PolicyManager.t.sol +++ b/test/unit/PolicyManager.t.sol @@ -36,7 +36,7 @@ contract PolicyManagerTest is Test { MockClaimManager internal mockClaimManager; MockCoverPoolFactory internal mockCoverPoolFactory; MockBindPolicyHook internal defaultHook; - MockChainlinkPriceFeed internal mockPriceFeed; + MockOraclePriceFeed internal mockPriceFeed; bytes32 internal policyCommit; bytes32 internal specId; @@ -77,7 +77,7 @@ contract PolicyManagerTest is Test { mockClaimManager = new MockClaimManager(); mockCoverPoolFactory = new MockCoverPoolFactory(); mockCoverPoolFactory.registerPool(address(coverPool)); - mockPriceFeed = new MockChainlinkPriceFeed(); + mockPriceFeed = new MockOraclePriceFeed(); policyCommit = keccak256("policy-commit"); specId = keccak256("spec-id"); @@ -161,7 +161,7 @@ contract PolicyManagerTest is Test { ); } - function test_initialize_WhenChainlinkPriceFeedZero_Reverts() public { + function test_initialize_WhenOraclePriceFeedZero_Reverts() public { PolicyManager implementation = new PolicyManager(); PolicyManager freshManager = PolicyManager(address(new ERC1967Proxy(address(implementation), ""))); address[] memory empty = new address[](0); @@ -505,9 +505,9 @@ contract PolicyManagerTest is Test { mockStakeManager.setCommitteeVaults(committeeId, vaults, COVERAGE_LIMIT); mockStakeManager.setOperatorCommitteeStake(committeeId, COVERAGE_LIMIT); - MockChainlinkPriceFeedZero zeroFeed = new MockChainlinkPriceFeedZero(request.payoutToken); + MockOraclePriceFeedZero zeroFeed = new MockOraclePriceFeedZero(request.payoutToken); vm.prank(admin); - policyManager.setChainlinkPriceFeed(address(zeroFeed)); + policyManager.setOraclePriceFeed(address(zeroFeed)); ICoverPool.BoundPolicy memory boundPolicy = _getBoundPolicy(request); vm.expectRevert(IPolicyManager.ZeroCoverageLimitUSD.selector); @@ -1076,10 +1076,10 @@ contract PolicyManagerTest is Test { } /*////////////////////////////////////////////////////////////// - SET CHAINLINK PRICE FEED + SET ORACLE PRICE FEED //////////////////////////////////////////////////////////////*/ - function test_setChainlinkPriceFeed_WhenCallerNotAdmin_Reverts() public { + function test_setOraclePriceFeed_WhenCallerNotAdmin_Reverts() public { address newFeed = makeAddr("newFeed"); vm.expectRevert( abi.encodeWithSelector( @@ -1087,25 +1087,25 @@ contract PolicyManagerTest is Test { ) ); vm.prank(buyer); - policyManager.setChainlinkPriceFeed(newFeed); + policyManager.setOraclePriceFeed(newFeed); } - function test_setChainlinkPriceFeed_WhenAddressZero_Reverts() public { + function test_setOraclePriceFeed_WhenAddressZero_Reverts() public { vm.expectRevert(IPolicyManager.ZeroAddress.selector); vm.prank(admin); - policyManager.setChainlinkPriceFeed(address(0)); + policyManager.setOraclePriceFeed(address(0)); } - function test_setChainlinkPriceFeed_WhenValidAddress_UpdatesStateAndEmits() public { + function test_setOraclePriceFeed_WhenValidAddress_UpdatesStateAndEmits() public { address newFeed = makeAddr("newFeed"); vm.expectEmit(true, true, true, true); - emit IPolicyManager.ChainlinkPriceFeedSet(newFeed); + emit IPolicyManager.OraclePriceFeedSet(newFeed); vm.prank(admin); - policyManager.setChainlinkPriceFeed(newFeed); + policyManager.setOraclePriceFeed(newFeed); - assertEq(policyManager.chainlinkPriceFeed(), newFeed, "chainlinkPriceFeed not updated"); + assertEq(policyManager.oraclePriceFeed(), newFeed, "oraclePriceFeed not updated"); } /*////////////////////////////////////////////////////////////// @@ -2099,7 +2099,7 @@ contract MockRevertingHook { } } -contract MockChainlinkPriceFeed { +contract MockOraclePriceFeed { function getUSDValue(address, uint256 amount) external pure returns (uint256) { return amount; } @@ -2109,7 +2109,7 @@ contract MockChainlinkPriceFeed { } } -contract MockChainlinkPriceFeedZero { +contract MockOraclePriceFeedZero { address public zeroToken; constructor(address zeroToken_) { From 255defd9b61929022b039739e69378d98795d67a Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Fri, 8 May 2026 14:06:19 +0200 Subject: [PATCH 06/11] chore: fix stale chainlink references --- script/VerifySepoliaConfig.s.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/VerifySepoliaConfig.s.sol b/script/VerifySepoliaConfig.s.sol index 97f17202..03120d74 100644 --- a/script/VerifySepoliaConfig.s.sol +++ b/script/VerifySepoliaConfig.s.sol @@ -42,7 +42,7 @@ interface IOraclePriceFeedView { } interface IAdapterView { - function chainlinkPriceFeed() external view returns (address); + function oraclePriceFeed() external view returns (address); } interface IPolicyManagerView { @@ -198,7 +198,7 @@ contract VerifySepoliaConfig is Script { _check( "SSPRouter.getAdapter(EIGENLAYER)", ISSPRouterView(SSP_ROUTER).getAdapter(MODULE_EIGENLAYER), EIGEN_ADAPTER ); - _check("EigenAdapter.chainlinkPriceFeed", IAdapterView(EIGEN_ADAPTER).chainlinkPriceFeed(), ORACLE_PRICE_FEED); + _check("EigenAdapter.oraclePriceFeed", IAdapterView(EIGEN_ADAPTER).oraclePriceFeed(), ORACLE_PRICE_FEED); console2.log(""); } From 2e1a5a088796bf7d353223306723e4f1cc584632 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Fri, 8 May 2026 15:10:56 +0200 Subject: [PATCH 07/11] fix: slash inflation doesn't cover oracle-DEX deviation gap --- src/ClaimManager.sol | 19 +++++---- test/unit/ClaimManager.t.sol | 76 ++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/src/ClaimManager.sol b/src/ClaimManager.sol index a9c8c8ab..b73f3b2c 100644 --- a/src/ClaimManager.sol +++ b/src/ClaimManager.sol @@ -605,8 +605,6 @@ contract ClaimManager is * @dev Two-pass: values each stake via oracle fair value, then computes proportional * slash amounts. The DEX quote is used as a bounded sanity check — if it deviates from the oracle * fair value by more than `priceDeviationToleranceBps`, the whole claim reverts. - * Cross-token slashes are inflated by MAX / (MAX - maxSwapSlippageBps) so the beneficiary receives - * the full requestedAmount even at worst-case swap slippage; same-token vaults need no inflation. * @param vaults Vault addresses from previewSlashing. * @param tokens Collateral token address per vault. * @param tokenStakes Token-native stake amount per vault. @@ -637,9 +635,6 @@ contract ClaimManager is if (tokens[i] == payoutToken) { payoutEquivalents[i] = tokenStakes[i]; } else { - // Oracle fair value is authoritative for slash distribution sizing. - // The DEX quote is a bounded sanity check: revert if it deviates too far from fair value, - // indicating a manipulated or illiquid pool. // slither-disable-next-line calls-loop uint256 fair = _oracleFairValue(tokens[i], payoutToken, tokenStakes[i]); @@ -673,9 +668,17 @@ contract ClaimManager is // slither-disable-next-line divide-before-multiply uint256 baseSlash = (tokenStakes[i] * requestedAmount) / totalPayoutEquivalent; - if (tokens[i] != payoutToken && cachedMaxSwapSlippageBps > 0) { - uint256 maxSlippage = uint256(_MAX_SLIPPAGE_BPS); - baseSlash = (baseSlash * maxSlippage) / (maxSlippage - cachedMaxSwapSlippageBps); + if (tokens[i] != payoutToken) { + { + uint256 maxSlippage = uint256(_MAX_SLIPPAGE_BPS); + if (cachedMaxSwapSlippageBps > 0) { + baseSlash = (baseSlash * maxSlippage) / (maxSlippage - cachedMaxSwapSlippageBps); + } + uint256 deviationBps = priceDeviationToleranceBps; + if (deviationBps > 0) { + baseSlash = (baseSlash * maxSlippage) / (maxSlippage - deviationBps); + } + } } if (baseSlash == 0) { diff --git a/test/unit/ClaimManager.t.sol b/test/unit/ClaimManager.t.sol index a7460a02..655b9a94 100644 --- a/test/unit/ClaimManager.t.sol +++ b/test/unit/ClaimManager.t.sol @@ -2205,7 +2205,8 @@ contract ClaimManagerTest is Test { uint256 stake = 200 ether; uint16 slippageBps = claimManager.maxSwapSlippageBps(); - uint256 inflatedSlash = requestedAmount * 10_000 / (10_000 - slippageBps); + uint16 deviationBps = claimManager.priceDeviationToleranceBps(); + uint256 inflatedSlash = requestedAmount * 10_000 / (10_000 - slippageBps) * 10_000 / (10_000 - deviationBps); MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); address[] memory pvaults = new address[](1); @@ -2237,6 +2238,51 @@ contract ClaimManagerTest is Test { ); } + function test_fileClaim_WhenOracleOverestimatesDex_CompoundInflationCoversDeviationGap() public { + uint256 requestedAmount = 100 ether; + uint256 stake = 200 ether; + + oracleFeedMock.setPrice(collateralToken, 1_030_000_000_000_000_000); // oracle: 1.03 payout per collateral + + uint256 fair = stake * 1_030_000_000_000_000_000 / 1e18; // 206 ether (oracle fair for full stake) + uint256 s = claimManager.maxSwapSlippageBps(); + uint256 d = claimManager.priceDeviationToleranceBps(); + uint256 inflatedSlash = stake * requestedAmount / fair * 10_000 / (10_000 - s) * 10_000 / (10_000 - d); + assertGt(inflatedSlash, requestedAmount, "Compound inflation must exceed requestedAmount to cover oracle gap"); + + MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); + address[] memory pvaults = new address[](1); + pvaults[0] = makeAddr("vault"); + address[] memory ptokens = new address[](1); + ptokens[0] = collateralToken; + uint256[] memory pstakes = new uint256[](1); + pstakes[0] = stake; + slashMock.setupPreview(pvaults, ptokens, pstakes); + collateralTokenMock.mint(address(slashMock), inflatedSlash); + vm.prank(admin); + claimManager.setSlashingManager(address(slashMock)); + + swapperMock.setPayoutToken(payoutToken); + swapperMock.setSwapOutput(stake); + + specMock.setEvaluationResult(true, keccak256("approved")); + + uint256 balanceBefore = payoutTokenMock.balanceOf(beneficiary); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); + + assertEq( + payoutTokenMock.balanceOf(beneficiary) - balanceBefore, + requestedAmount, + "Beneficiary must receive full requestedAmount despite oracle overestimation" + ); + assertEq( + collateralTokenMock.balanceOf(address(slashMock)), + 0, + "All compound-inflated collateral must be transferred from slash mock" + ); + } + function test_fileClaim_WhenSwapProducesMoreThanRemainingPayout_SurplusDistributedAsRewards() public { uint256 requestedAmount = 100 ether; @@ -2250,7 +2296,9 @@ contract ClaimManagerTest is Test { pstakes[0] = 200 ether; slashMock.setupPreview(pvaults, ptokens, pstakes); collateralTokenMock.mint( - address(slashMock), requestedAmount * 10_000 / (10_000 - claimManager.maxSwapSlippageBps()) + address(slashMock), + requestedAmount * 10_000 / (10_000 - claimManager.maxSwapSlippageBps()) * 10_000 + / (10_000 - claimManager.priceDeviationToleranceBps()) ); } @@ -2305,7 +2353,8 @@ contract ClaimManagerTest is Test { slashMock.setupPreview(pvaults, ptokens, pstakes); uint16 slippageBps = claimManager.maxSwapSlippageBps(); - uint256 inflatedSlash = requestedAmount * 10_000 / (10_000 - slippageBps); + uint16 deviationBps = claimManager.priceDeviationToleranceBps(); + uint256 inflatedSlash = requestedAmount * 10_000 / (10_000 - slippageBps) * 10_000 / (10_000 - deviationBps); collateralTokenMock.mint(address(slashMock), inflatedSlash); vm.prank(admin); @@ -2339,7 +2388,8 @@ contract ClaimManagerTest is Test { slashMock.setupPreview(pvaults, ptokens, pstakes); uint16 slippageBps = claimManager.maxSwapSlippageBps(); - uint256 inflatedSlash = requestedAmount * 10_000 / (10_000 - slippageBps); + uint16 deviationBps = claimManager.priceDeviationToleranceBps(); + uint256 inflatedSlash = requestedAmount * 10_000 / (10_000 - slippageBps) * 10_000 / (10_000 - deviationBps); collateralTokenMock.mint(address(slashMock), inflatedSlash); vm.prank(admin); @@ -2885,7 +2935,8 @@ contract ClaimManagerTest is Test { slashMock.setupPreview(pvaults, ptokens, pstakes); uint256 baseSlash = (largeStake * requestedAmount) / (tinyStake + largeStake); - uint256 expectedLargeSlash = (baseSlash * 10_000) / (10_000 - claimManager.maxSwapSlippageBps()); + uint256 expectedLargeSlash = baseSlash * 10_000 / (10_000 - claimManager.maxSwapSlippageBps()) * 10_000 + / (10_000 - claimManager.priceDeviationToleranceBps()); collateralTokenMock.mint(address(slashMock), expectedLargeSlash); vm.prank(admin); claimManager.setSlashingManager(address(slashMock)); @@ -2957,7 +3008,8 @@ contract ClaimManagerTest is Test { uint256 totalEquiv = tinyStake / DIVISOR + largeStake; uint256 largeBaseSlash = (largeStake * requestedAmount) / totalEquiv; - uint256 expectedLargeSlash = (largeBaseSlash * 10_000) / (10_000 - claimManager.maxSwapSlippageBps()); + uint256 expectedLargeSlash = largeBaseSlash * 10_000 / (10_000 - claimManager.maxSwapSlippageBps()) * 10_000 + / (10_000 - claimManager.priceDeviationToleranceBps()); collateralTokenMock.mint(address(slashMock), expectedLargeSlash); vm.prank(admin); claimManager.setSlashingManager(address(slashMock)); @@ -3156,6 +3208,8 @@ contract ClaimManagerTest is Test { vm.prank(admin); claimManager.setMaxSwapSlippageBps(0); + vm.prank(admin); + claimManager.setPriceDeviationToleranceBps(0); // no inflation; test focuses on vault-skip behavior collateralTokenMock.mint(address(slashMock), crossSlash); payoutTokenMock.mint(address(slashMock), sameSlash); @@ -3191,6 +3245,8 @@ contract ClaimManagerTest is Test { uint256 stake = 100 ether; vm.prank(admin); claimManager.setMaxSwapSlippageBps(0); + vm.prank(admin); + claimManager.setPriceDeviationToleranceBps(0); // no inflation; test focuses on vault-skip behavior MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); address[] memory pvaults = new address[](2); @@ -3245,6 +3301,8 @@ contract ClaimManagerTest is Test { uint256 stake = 100 ether; vm.prank(admin); claimManager.setMaxSwapSlippageBps(0); + vm.prank(admin); + claimManager.setPriceDeviationToleranceBps(0); // no inflation; test focuses on vault-skip behavior MockToken collateralToken2Mock = new MockToken("Collateral2", "COL2"); address collateralToken2 = address(collateralToken2Mock); @@ -3313,6 +3371,8 @@ contract ClaimManagerTest is Test { uint256 stake = 100 ether; vm.prank(admin); claimManager.setMaxSwapSlippageBps(0); + vm.prank(admin); + claimManager.setPriceDeviationToleranceBps(0); // no inflation; test focuses on vault-skip behavior MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); address[] memory pvaults = new address[](2); @@ -3372,7 +3432,9 @@ contract ClaimManagerTest is Test { pstakes[0] = 200 ether; slashMock.setupPreview(pvaults, ptokens, pstakes); collateralTokenMock.mint( - address(slashMock), requestedAmount * 10_000 / (10_000 - claimManager.maxSwapSlippageBps()) + address(slashMock), + requestedAmount * 10_000 / (10_000 - claimManager.maxSwapSlippageBps()) * 10_000 + / (10_000 - claimManager.priceDeviationToleranceBps()) ); } vm.prank(admin); From 7e8430dae4ea61fb938acfc1898d0031f338b08e Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Fri, 8 May 2026 15:22:43 +0200 Subject: [PATCH 08/11] fix: incorporate USD conversion in CoverPool using IOraclePriceFeed --- src/CoverPool.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/CoverPool.sol b/src/CoverPool.sol index 00324226..e088b484 100644 --- a/src/CoverPool.sol +++ b/src/CoverPool.sol @@ -14,7 +14,7 @@ import {IPolicyManager} from "./interfaces/IPolicyManager.sol"; import {ISpec} from "./interfaces/ISpec.sol"; import {ISpecRegistry} from "./interfaces/ISpecRegistry.sol"; import {IStakeManager} from "./interfaces/IStakeManager.sol"; -import {IChainlinkPriceFeed} from "./interfaces/IChainlinkPriceFeed.sol"; +import {IOraclePriceFeed} from "./interfaces/IOraclePriceFeed.sol"; /** * @title CoverPool @@ -218,9 +218,8 @@ contract CoverPool is AccessControlDefaultAdminRulesUpgradeable, EIP712Upgradeab // into core. SSPRouter._configureEigenStrategies expects USD to correctly size vault TVL limits. // Scoped block frees priceFeed and coverageLimitUSD from the stack before the bindPolicy call below. { - address priceFeed = IPolicyManager(policyManager).chainlinkPriceFeed(); - uint256 coverageLimitUSD = - IChainlinkPriceFeed(priceFeed).getUSDValue(draft.payoutToken, quote.coverageLimit); + address priceFeed = IPolicyManager(policyManager).oraclePriceFeed(); + uint256 coverageLimitUSD = IOraclePriceFeed(priceFeed).getUSDValue(draft.payoutToken, quote.coverageLimit); // Add vaults to committee in core contracts using policyId as committeeId (1:1 mapping). IStakeManager(stakeManager).setCommitteeVaults(draft.policyId, vaults, coverageLimitUSD); } From ba8bdc6fde5bf73d43f5ef47552fccd3cb904233 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Fri, 8 May 2026 15:34:26 +0200 Subject: [PATCH 09/11] test: fix failed unit test --- test/unit/CoverPool.t.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/CoverPool.t.sol b/test/unit/CoverPool.t.sol index 0b5d94eb..e7aa945e 100644 --- a/test/unit/CoverPool.t.sol +++ b/test/unit/CoverPool.t.sol @@ -80,7 +80,7 @@ contract CoverPoolTest is Test { mockPriceFeed = makeAddr("mockPriceFeed"); vm.label(mockPriceFeed, "MockPriceFeed"); - mockPolicyManager.setChainlinkPriceFeed(mockPriceFeed); + mockPolicyManager.setOraclePriceFeed(mockPriceFeed); vm.mockCall(mockPriceFeed, abi.encodeWithSignature("getUSDValue(address,uint256)"), abi.encode(uint256(1000e8))); implementation = new CoverPool(); @@ -1290,7 +1290,7 @@ contract CoverPoolTest is Test { contract MockPolicyManager { uint256 public nextPolicyId; uint256 public bindPolicyCallCount; - address public chainlinkPriceFeed; + address public oraclePriceFeed; IPolicyManager.BindPolicyRequest private _lastBindRequest; ICoverPool.BoundPolicy private _lastBoundPolicy; mapping(uint256 => IPolicyManager.PolicyDraft) private _drafts; @@ -1299,8 +1299,8 @@ contract MockPolicyManager { uint96 public lastPremiumDefaultedPolicyId; bool public lastPremiumDefaultedValue; - function setChainlinkPriceFeed(address _feed) external { - chainlinkPriceFeed = _feed; + function setOraclePriceFeed(address _feed) external { + oraclePriceFeed = _feed; } function bindPolicy( From e25fdf298ac2e4541c1abd33e72a37bbd03e3aaf Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Fri, 8 May 2026 16:19:13 +0200 Subject: [PATCH 10/11] fix: address slither issues --- src/ClaimManager.sol | 16 +++++++++------- test/unit/ClaimManager.t.sol | 5 +++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ClaimManager.sol b/src/ClaimManager.sol index b73f3b2c..5a08b087 100644 --- a/src/ClaimManager.sol +++ b/src/ClaimManager.sol @@ -344,6 +344,7 @@ contract ClaimManager is * @param amountIn Amount of tokenIn to value. * @return fair Token-native amount of tokenOut equivalent to amountIn of tokenIn at oracle prices. */ + // slither-disable-next-line cyclomatic-complexity function _oracleFairValue(address tokenIn, address tokenOut, uint256 amountIn) private view returns (uint256 fair) { address feed = oraclePriceFeed; if (feed == address(0)) revert OracleUnavailable(tokenIn, tokenOut); @@ -670,14 +671,15 @@ contract ClaimManager is uint256 baseSlash = (tokenStakes[i] * requestedAmount) / totalPayoutEquivalent; if (tokens[i] != payoutToken) { { - uint256 maxSlippage = uint256(_MAX_SLIPPAGE_BPS); - if (cachedMaxSwapSlippageBps > 0) { - baseSlash = (baseSlash * maxSlippage) / (maxSlippage - cachedMaxSwapSlippageBps); - } uint256 deviationBps = priceDeviationToleranceBps; - if (deviationBps > 0) { - baseSlash = (baseSlash * maxSlippage) / (maxSlippage - deviationBps); - } + // slither-disable-next-line divide-before-multiply + baseSlash = (baseSlash * uint256(_MAX_SLIPPAGE_BPS) * uint256(_MAX_SLIPPAGE_BPS)) + / ((cachedMaxSwapSlippageBps > 0 + ? uint256(_MAX_SLIPPAGE_BPS) - cachedMaxSwapSlippageBps + : uint256(_MAX_SLIPPAGE_BPS)) + * (deviationBps > 0 + ? uint256(_MAX_SLIPPAGE_BPS) - deviationBps + : uint256(_MAX_SLIPPAGE_BPS))); } } diff --git a/test/unit/ClaimManager.t.sol b/test/unit/ClaimManager.t.sol index 655b9a94..2d872213 100644 --- a/test/unit/ClaimManager.t.sol +++ b/test/unit/ClaimManager.t.sol @@ -2935,8 +2935,9 @@ contract ClaimManagerTest is Test { slashMock.setupPreview(pvaults, ptokens, pstakes); uint256 baseSlash = (largeStake * requestedAmount) / (tinyStake + largeStake); - uint256 expectedLargeSlash = baseSlash * 10_000 / (10_000 - claimManager.maxSwapSlippageBps()) * 10_000 - / (10_000 - claimManager.priceDeviationToleranceBps()); + uint256 slippageDenom = 10_000 - claimManager.maxSwapSlippageBps(); + uint256 deviationDenom = 10_000 - claimManager.priceDeviationToleranceBps(); + uint256 expectedLargeSlash = (baseSlash * 10_000 * 10_000) / (slippageDenom * deviationDenom); collateralTokenMock.mint(address(slashMock), expectedLargeSlash); vm.prank(admin); claimManager.setSlashingManager(address(slashMock)); From fa0f51a06f8469ed02e8003b78b6f2a0eeb9a4c5 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Sat, 9 May 2026 13:14:29 +0200 Subject: [PATCH 11/11] test: add new unit tests for ClaimManager --- test/unit/ClaimManager.t.sol | 366 ++++++++++++++++++++--------------- 1 file changed, 212 insertions(+), 154 deletions(-) diff --git a/test/unit/ClaimManager.t.sol b/test/unit/ClaimManager.t.sol index 2d872213..08f6c6d7 100644 --- a/test/unit/ClaimManager.t.sol +++ b/test/unit/ClaimManager.t.sol @@ -220,9 +220,8 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - INITIALIZE V2 + INITIALIZE V2 //////////////////////////////////////////////////////////////*/ - function test_initializeV2_WhenZeroOraclePriceFeed_Reverts() public { ClaimManager impl = new ClaimManager(); ClaimManager cm = ClaimManager(payable(address(new ERC1967Proxy(address(impl), "")))); @@ -263,9 +262,8 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - SET ORACLE PRICE FEED + SET ORACLE PRICE FEED //////////////////////////////////////////////////////////////*/ - function test_setOraclePriceFeed_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( abi.encodeWithSelector( @@ -294,9 +292,8 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - SET PRICE DEVIATION TOLERANCE BPS + SET PRICE DEVIATION TOLERANCE BPS //////////////////////////////////////////////////////////////*/ - function test_setPriceDeviationToleranceBps_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( abi.encodeWithSelector( @@ -330,7 +327,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - PAUSE / UNPAUSE + PAUSE / UNPAUSE //////////////////////////////////////////////////////////////*/ function test_pause_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( @@ -386,7 +383,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - SET SPEC REGISTRY + SET SPEC REGISTRY //////////////////////////////////////////////////////////////*/ function test_setSpecRegistry_WhenCallerNotAdmin_Reverts() public { address newSpecRegistry = makeAddr("newSpecRegistry"); @@ -420,7 +417,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - SET SLASHING MANAGER + SET SLASHING MANAGER //////////////////////////////////////////////////////////////*/ function test_setSlashingManager_WhenCallerNotAdmin_Reverts() public { address newSlashingManager = makeAddr("newSlashingManager"); @@ -454,7 +451,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - SET SWAPPER + SET SWAPPER //////////////////////////////////////////////////////////////*/ function test_setSwapper_WhenCallerNotAdmin_Reverts() public { address newSwapper = makeAddr("newSwapper"); @@ -488,7 +485,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - SET POLICY MANAGER + SET POLICY MANAGER //////////////////////////////////////////////////////////////*/ function test_setPolicyManager_WhenCallerNotAdmin_Reverts() public { address newPolicyManager = makeAddr("newPolicyManager"); @@ -738,7 +735,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - RECOVER TOKENS + RECOVER TOKENS //////////////////////////////////////////////////////////////*/ function test_receive_AcceptsNativeETH() public { uint256 amount = 1 ether; @@ -807,12 +804,7 @@ contract ClaimManagerTest is Test { claimManager.recoverTokens(collateralToken, beneficiary, 0); } - /*////////////////////////////////////////////////////////////// - FILE CLAIM — ORACLE PROTECTION (CYS3-03) - //////////////////////////////////////////////////////////////*/ - function test_fileClaim_WhenOraclePriceFeedUnset_RevertsOracleUnavailable() public { - // Deploy a fresh ClaimManager that was never given initializeV2 (oraclePriceFeed == address(0)) Options memory opts; opts.unsafeSkipAllChecks = true; address freshProxy = Upgrades.deployUUPSProxy( @@ -822,10 +814,7 @@ contract ClaimManagerTest is Test { ); ClaimManager freshCm = ClaimManager(payable(freshProxy)); - // Wire policy to fresh proxy policyMock.setPolicyMetadata(POLICY_ID, _defaultMetadata()); - - // collateralToken != payoutToken → cross-token path → OracleUnavailable (feed address is address(0)) specMock.setEvaluationResult(true, keccak256("approved")); address[] memory assets = new address[](1); assets[0] = collateralToken; @@ -883,12 +872,57 @@ contract ClaimManagerTest is Test { claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); } + /*////////////////////////////////////////////////////////////// + FILE CLAIM — ORACLE PROTECTION (CYS3-03) + //////////////////////////////////////////////////////////////*/ + function test_fileClaim_WhenGetUSDValueReturnsZero_RevertsOracleUnavailable() public { + oracleFeedMock.setUSDValueZero(collateralToken); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert(abi.encodeWithSelector(IClaimManager.OracleUnavailable.selector, collateralToken, payoutToken)); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + + function test_fileClaim_WhenGetTokenAmountReverts_RevertsOracleUnavailable() public { + oracleFeedMock.setGetTokenAmountShouldRevert(true); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert(abi.encodeWithSelector(IClaimManager.OracleUnavailable.selector, collateralToken, payoutToken)); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + + function test_fileClaim_WhenGetTokenAmountReturnsZero_RevertsOracleUnavailable() public { + oracleFeedMock.setTokenAmountZero(payoutToken); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = SLASH_AMOUNT; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert(abi.encodeWithSelector(IClaimManager.OracleUnavailable.selector, collateralToken, payoutToken)); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, ""); + } + function test_fileClaim_WhenDexQuoteDeviatesAboveTolerance_RevertsQuoteDeviatesFromOracle() public { - // Configure a swapper where quoteSwap for collateralToken returns 5000 ether - // but oracle fair value is 1:1 = SLASH_AMOUNT. - // Deviation = (5000 ether - 500 ether) / 500 ether * 10000 = 90000 bps >> 300 bps tolerance. MockMultiSwapper deviatingSwapper = new MockMultiSwapper(); - deviatingSwapper.setQuoteOutput(collateralToken, 5000 ether); // quoteSwap returns 5000 ether + deviatingSwapper.setQuoteOutput(collateralToken, 5000 ether); deviatingSwapper.setSwapOutput(collateralToken, 5000 ether); deviatingSwapper.setPayoutToken(payoutToken); vm.prank(admin); @@ -901,7 +935,6 @@ contract ClaimManagerTest is Test { amounts[0] = SLASH_AMOUNT; slashingManagerMock.setSlashResult(assets, amounts); - // oracleFair = SLASH_AMOUNT (1:1); dex = 5000 ether → huge upward deviation vm.expectRevert( abi.encodeWithSelector( IClaimManager.QuoteDeviatesFromOracle.selector, @@ -917,11 +950,8 @@ contract ClaimManagerTest is Test { } function test_fileClaim_WhenDexQuoteDeviatesBelowTolerance_RevertsQuoteDeviatesFromOracle() public { - // Configure a swapper where quoteSwap for collateralToken returns 400 ether (20% below 500) - // but oracle fair value is 1:1 = SLASH_AMOUNT = 500 ether. - // Deviation = (500 - 400) / 500 * 10000 = 2000 bps >> 300 bps tolerance. MockMultiSwapper deviatingSwapper = new MockMultiSwapper(); - deviatingSwapper.setQuoteOutput(collateralToken, 400 ether); // quoteSwap returns 400 ether + deviatingSwapper.setQuoteOutput(collateralToken, 400 ether); deviatingSwapper.setSwapOutput(collateralToken, 400 ether); deviatingSwapper.setPayoutToken(payoutToken); vm.prank(admin); @@ -949,20 +979,15 @@ contract ClaimManagerTest is Test { } function test_fileClaim_WhenDexQuoteWithinTolerance_SlashSizingUsesOracleFair() public { - // DEX quote and oracle fair are both 1:1 (deviation = 0 bps → well within 300 bps tolerance). - // Slash sizing uses payoutEquivalents[i] = _oracleFairValue(token, payout, stake) = stake. - // The proportional slash is: baseSlash = stake * requested / stake = requested, inflated by slippage. uint256 stakeA = 300 ether; uint256 requestedAmount = 100 ether; - // Default quoteSwap is 1:1; executeSwap mints requestedAmount to beneficiary. MockMultiSwapper multiSwap = new MockMultiSwapper(); multiSwap.setSwapOutput(collateralToken, requestedAmount); multiSwap.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(multiSwap)); - // Single cross-token vault: stakeA of collateralToken MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); address[] memory pvaults = new address[](1); pvaults[0] = makeAddr("vA"); @@ -971,7 +996,6 @@ contract ClaimManagerTest is Test { uint256[] memory pstakes = new uint256[](1); pstakes[0] = stakeA; slashMock.setupPreview(pvaults, ptokens, pstakes); - // ~102 ether of collateral will be slashed (100 requested inflated by 2% slippage buffer) collateralTokenMock.mint(address(slashMock), stakeA); vm.prank(admin); claimManager.setSlashingManager(address(slashMock)); @@ -983,18 +1007,15 @@ contract ClaimManagerTest is Test { uint256 claimId = claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); assertEq(uint8(claimManager.claimRecord(POLICY_ID, claimId).status), uint8(IClaimManager.ClaimStatus.Approved)); - // Beneficiary receives up to requestedAmount (excess is surplus-distributed or stranded) assertGt(payoutTokenMock.balanceOf(beneficiary) - balanceBefore, 0, "Beneficiary should receive payout"); } function test_fileClaim_WhenDexBelowFairWithinTolerance_AmountOutMinAnchoredToDex() public { - // DEX 1.5% below fair → anchor = dex; amountOutMin = dex * (1 - slippage) - // The swap should succeed because amountOutMin is achievable. - uint256 dexQuote = (SLASH_AMOUNT * 9850) / 10_000; // 1.5% below + uint256 dexQuote = (SLASH_AMOUNT * 9850) / 10_000; MockMultiSwapper nearFairSwapper = new MockMultiSwapper(); - nearFairSwapper.setQuoteOutput(collateralToken, dexQuote); // quoteSwap returns dexQuote - nearFairSwapper.setSwapOutput(collateralToken, dexQuote); // executeSwap also returns dexQuote + nearFairSwapper.setQuoteOutput(collateralToken, dexQuote); + nearFairSwapper.setSwapOutput(collateralToken, dexQuote); nearFairSwapper.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(nearFairSwapper)); @@ -1014,13 +1035,11 @@ contract ClaimManagerTest is Test { } function test_fileClaim_WhenDexAboveFairWithinTolerance_AmountOutMinAnchoredToFair() public { - // DEX 1.5% above fair → anchor = fair; amountOutMin = fair * (1 - slippage) - // The swap should succeed because the swap produces at least the fair-anchored minimum. - uint256 dexQuote = (SLASH_AMOUNT * 10_150) / 10_000; // 1.5% above + uint256 dexQuote = (SLASH_AMOUNT * 10_150) / 10_000; MockMultiSwapper aboveFairSwapper = new MockMultiSwapper(); - aboveFairSwapper.setQuoteOutput(collateralToken, dexQuote); // quoteSwap returns above-fair - aboveFairSwapper.setSwapOutput(collateralToken, dexQuote); // executeSwap produces dexQuote + aboveFairSwapper.setQuoteOutput(collateralToken, dexQuote); + aboveFairSwapper.setSwapOutput(collateralToken, dexQuote); aboveFairSwapper.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(aboveFairSwapper)); @@ -1040,13 +1059,9 @@ contract ClaimManagerTest is Test { } function test_fileClaim_NativeEthCollateral_NormalizesToWrapperForOracle() public { - // When the collateral is NATIVE_ETH, _oracleFairValue must query nativeWrapper not NATIVE_ETH. - // Mark NATIVE_ETH as missing but nativeWrapper as present in the mock feed. address nativeWrapper = swapperMock.nativeWrapper(); - oracleFeedMock.setFeedMissing(NATIVE_ETH); // ensures we only pass via nativeWrapper path + oracleFeedMock.setFeedMissing(NATIVE_ETH); - // Set up a policy where payoutToken == nativeWrapper so same-token path is taken for NATIVE_ETH - // (after normalization, NATIVE_ETH → nativeWrapper == payoutToken → same-token path → no oracle needed) IPolicyManager.PolicyMetadata memory meta = _defaultMetadata(); meta.payoutToken = nativeWrapper; meta.claimer = claimer; @@ -1066,16 +1081,8 @@ contract ClaimManagerTest is Test { vm.prank(admin); claimManager.setSlashingManager(address(slashMock)); - // payoutToken == nativeWrapper; NATIVE_ETH normalizes to nativeWrapper → same-token path → no oracle needed - // slashMock sends ETH; claimManager receives it and transfers to beneficiary (native ETH payout) - // This test verifies that NATIVE_ETH → nativeWrapper normalization does NOT trigger OracleUnavailable - // even when the NATIVE_ETH sentinel's feed is marked missing. vm.prank(claimer); - // Expected: no OracleUnavailable revert (NATIVE_ETH → nativeWrapper → same-token path) - // May revert for other reasons (native transfer), so just assert no oracle error: - try claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, "") { - // success is fine - } + try claimManager.fileClaim(POLICY_ID, SLASH_AMOUNT, evidenceHash, "") {} catch (bytes memory data) { bytes4 oracleSelector = IClaimManager.OracleUnavailable.selector; if (data.length >= 4) { @@ -1088,6 +1095,107 @@ contract ClaimManagerTest is Test { } } + function test_fileClaim_WhenDeviationExactlyAtBoundary_Succeeds() public { + uint256 stake = 10_000 ether; + uint256 dexQuote = 10_300 ether; + + IPolicyManager.PolicyMetadata memory meta = _defaultMetadata(); + meta.coverageLimit = stake; + policyMock.setPolicyMetadata(POLICY_ID, meta); + + MockMultiSwapper boundarySwapper = new MockMultiSwapper(); + boundarySwapper.setQuoteOutput(collateralToken, dexQuote); + boundarySwapper.setSwapOutput(collateralToken, stake); + boundarySwapper.setPayoutToken(payoutToken); + vm.prank(admin); + claimManager.setSwapper(address(boundarySwapper)); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = stake; + slashingManagerMock.setSlashResult(assets, amounts); + collateralTokenMock.mint(address(slashingManagerMock), stake); + slashingManagerMock.setTokenToTransfer(collateralToken); + + vm.prank(claimer); + uint256 claimId = claimManager.fileClaim(POLICY_ID, stake, evidenceHash, ""); + assertEq(uint8(claimManager.claimRecord(POLICY_ID, claimId).status), uint8(IClaimManager.ClaimStatus.Approved)); + } + + function test_fileClaim_WhenDeviationOneBpsAboveBoundary_RevertsQuoteDeviatesFromOracle() public { + uint256 stake = 10_000 ether; + uint256 dexQuote = 10_301 ether; + + IPolicyManager.PolicyMetadata memory meta = _defaultMetadata(); + meta.coverageLimit = stake; + policyMock.setPolicyMetadata(POLICY_ID, meta); + + MockMultiSwapper offByOneSwapper = new MockMultiSwapper(); + offByOneSwapper.setQuoteOutput(collateralToken, dexQuote); + offByOneSwapper.setSwapOutput(collateralToken, stake); + offByOneSwapper.setPayoutToken(payoutToken); + vm.prank(admin); + claimManager.setSwapper(address(offByOneSwapper)); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = stake; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert( + abi.encodeWithSelector( + IClaimManager.QuoteDeviatesFromOracle.selector, + collateralToken, + payoutToken, + dexQuote, + stake, + uint256(301) + ) + ); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, stake, evidenceHash, ""); + } + + function test_fileClaim_WhenZeroDeviationTolerance_AnyDeviationReverts() public { + vm.prank(admin); + claimManager.setPriceDeviationToleranceBps(0); + + uint256 stake = 500 ether; + + uint256 dexQuote = stake + 1 ether; + + MockMultiSwapper zeroTolSwapper = new MockMultiSwapper(); + zeroTolSwapper.setQuoteOutput(collateralToken, dexQuote); + zeroTolSwapper.setSwapOutput(collateralToken, stake); + zeroTolSwapper.setPayoutToken(payoutToken); + vm.prank(admin); + claimManager.setSwapper(address(zeroTolSwapper)); + + specMock.setEvaluationResult(true, keccak256("approved")); + address[] memory assets = new address[](1); + assets[0] = collateralToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = stake; + slashingManagerMock.setSlashResult(assets, amounts); + + vm.expectRevert( + abi.encodeWithSelector( + IClaimManager.QuoteDeviatesFromOracle.selector, + collateralToken, + payoutToken, + dexQuote, + stake, + uint256(20) + ) + ); + vm.prank(claimer); + claimManager.fileClaim(POLICY_ID, stake, evidenceHash, ""); + } + /*////////////////////////////////////////////////////////////// FILE CLAIM //////////////////////////////////////////////////////////////*/ @@ -1518,7 +1626,7 @@ contract ClaimManagerTest is Test { MockMultiSwapper multiSwapper = new MockMultiSwapper(); multiSwapper.setSwapOutput(collateralToken, firstSwapOutput); multiSwapper.setSwapOutput(collateralToken2, secondSwapOutput); - // quoteSwap defaults to 1:1 (amountIn) — oracle deviation gate passes automatically. + multiSwapper.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(multiSwapper)); @@ -2242,9 +2350,9 @@ contract ClaimManagerTest is Test { uint256 requestedAmount = 100 ether; uint256 stake = 200 ether; - oracleFeedMock.setPrice(collateralToken, 1_030_000_000_000_000_000); // oracle: 1.03 payout per collateral + oracleFeedMock.setPrice(collateralToken, 1_030_000_000_000_000_000); - uint256 fair = stake * 1_030_000_000_000_000_000 / 1e18; // 206 ether (oracle fair for full stake) + uint256 fair = stake * 1_030_000_000_000_000_000 / 1e18; uint256 s = claimManager.maxSwapSlippageBps(); uint256 d = claimManager.priceDeviationToleranceBps(); uint256 inflatedSlash = stake * requestedAmount / fair * 10_000 / (10_000 - s) * 10_000 / (10_000 - d); @@ -2429,7 +2537,7 @@ contract ClaimManagerTest is Test { slashingManagerMock.setTokenToTransfer(collateralToken); swapperMock.setPayoutToken(payoutToken); - swapperMock.setSwapOutput(requestedAmount); // exactly matches — no surplus + swapperMock.setSwapOutput(requestedAmount); vm.prank(claimer); claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); @@ -2684,7 +2792,7 @@ contract ClaimManagerTest is Test { MockNativeMultiSwapper nativeMultiSwapper = new MockNativeMultiSwapper(); nativeMultiSwapper.setSwapOutput(NATIVE_ETH, ethSwapOutput); nativeMultiSwapper.setSwapOutput(collateralToken, erc20SwapOutput); - // quoteSwap defaults to 1:1 (amountIn) — oracle deviation gate passes automatically. + nativeMultiSwapper.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(nativeMultiSwapper)); @@ -2901,8 +3009,6 @@ contract ClaimManagerTest is Test { specMock.setEvaluationResult(true, keccak256("approved")); - // Under the new oracle model, a missing DEX quote reverts the whole claim rather than - // silently excluding the vault from slashing distribution. vm.expectRevert(abi.encodeWithSelector(IClaimManager.QuoteUnavailable.selector, unquotableToken, payoutToken)); vm.prank(claimer); claimManager.fileClaim(POLICY_ID, requestedAmount, evidenceHash, ""); @@ -2956,22 +3062,19 @@ contract ClaimManagerTest is Test { function test_fileClaim_WhenDexQuoteDeviatesFromOracleByPerTokenDivisor_RevertsQuoteDeviatesFromOracle() public { (address tinyVaultAddr, address tinyToken) = _setupTinyBaseSlashWithRealisticQuoter(); - // Silence unused variable warning + tinyVaultAddr; specMock.setEvaluationResult(true, keccak256("approved")); - // The MockSwapperWithPerTokenDivisor divides the quote by 1001, so for tinyStake=1001: - // dex = 1001 / 1001 = 1; oracleFair = 1001 (1:1 mock). - // Deviation = (1000 / 1001) * 10000 ≈ 9990 bps > priceDeviationToleranceBps(300) → revert. vm.expectRevert( abi.encodeWithSelector( IClaimManager.QuoteDeviatesFromOracle.selector, tinyToken, payoutToken, - uint256(1), // dex quote (1001/1001) - uint256(1001), // oracle fair (1:1) - uint256(9990) // actual deviationBps + uint256(1), + uint256(1001), + uint256(9990) ) ); vm.prank(claimer); @@ -3193,7 +3296,7 @@ contract ClaimManagerTest is Test { uint256 crossSlash = 50 ether; uint256 sameSlash = 50 ether; uint256 crossSwapOutput = 70 ether; - uint256 expectedSameTokenSurplus = sameSlash - (requestedAmount - crossSwapOutput); // 50 - 30 = 20 + uint256 expectedSameTokenSurplus = sameSlash - (requestedAmount - crossSwapOutput); MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); address[] memory pvaults = new address[](2); @@ -3210,7 +3313,7 @@ contract ClaimManagerTest is Test { vm.prank(admin); claimManager.setMaxSwapSlippageBps(0); vm.prank(admin); - claimManager.setPriceDeviationToleranceBps(0); // no inflation; test focuses on vault-skip behavior + claimManager.setPriceDeviationToleranceBps(0); collateralTokenMock.mint(address(slashMock), crossSlash); payoutTokenMock.mint(address(slashMock), sameSlash); @@ -3247,7 +3350,7 @@ contract ClaimManagerTest is Test { vm.prank(admin); claimManager.setMaxSwapSlippageBps(0); vm.prank(admin); - claimManager.setPriceDeviationToleranceBps(0); // no inflation; test focuses on vault-skip behavior + claimManager.setPriceDeviationToleranceBps(0); MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); address[] memory pvaults = new address[](2); @@ -3303,7 +3406,7 @@ contract ClaimManagerTest is Test { vm.prank(admin); claimManager.setMaxSwapSlippageBps(0); vm.prank(admin); - claimManager.setPriceDeviationToleranceBps(0); // no inflation; test focuses on vault-skip behavior + claimManager.setPriceDeviationToleranceBps(0); MockToken collateralToken2Mock = new MockToken("Collateral2", "COL2"); address collateralToken2 = address(collateralToken2Mock); @@ -3328,7 +3431,7 @@ contract ClaimManagerTest is Test { MockMultiSwapper multiSwapper = new MockMultiSwapper(); multiSwapper.setSwapOutput(collateralToken, requestedAmount); multiSwapper.setSwapOutput(collateralToken2, requestedAmount); - // quoteSwap defaults to 1:1 (amountIn) so oracle deviation gate passes automatically. + multiSwapper.setPayoutToken(payoutToken); vm.prank(admin); claimManager.setSwapper(address(multiSwapper)); @@ -3352,10 +3455,7 @@ contract ClaimManagerTest is Test { requestedAmount, "Beneficiary must receive exactly requestedAmount" ); - // Cross-token surplus entries are intentionally skipped when payout is already filled. - // A surplus-only swap could revert (liquidity shift / price impact from the earlier swap) - // and block an already-completed beneficiary payout. The stranded collateral remains - // recoverable via recoverTokens(). + assertEq(premiumMock.totalReceived(), 0, "Cross-token surplus must not be attempted when payout is filled"); assertEq(payoutTokenMock.balanceOf(address(claimManager)), 0, "No payout tokens remain in ClaimManager"); assertEq( @@ -3373,7 +3473,7 @@ contract ClaimManagerTest is Test { vm.prank(admin); claimManager.setMaxSwapSlippageBps(0); vm.prank(admin); - claimManager.setPriceDeviationToleranceBps(0); // no inflation; test focuses on vault-skip behavior + claimManager.setPriceDeviationToleranceBps(0); MockSlashingManagerRespectingSlash slashMock = new MockSlashingManagerRespectingSlash(); address[] memory pvaults = new address[](2); @@ -3647,7 +3747,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - CLAIM RECORD + CLAIM RECORD //////////////////////////////////////////////////////////////*/ function test_claimRecord_WhenClaimNotFiled_ReturnsDefault() public view { IClaimManager.ClaimRecord memory record = claimManager.claimRecord(POLICY_ID, 0); @@ -3708,7 +3808,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - POLICY CLAIM + POLICY CLAIM //////////////////////////////////////////////////////////////*/ function test_policyClaim_WhenNoClaims_ReturnsDefaults() public view { IClaimManager.PolicyClaim memory state = claimManager.policyClaim(POLICY_ID); @@ -3923,7 +4023,7 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - HELPERS + HELPERS //////////////////////////////////////////////////////////////*/ function _setupDefaultPolicy() private { IPolicyManager.PolicyMetadata memory metadata = _defaultMetadata(); @@ -3971,9 +4071,8 @@ contract ClaimManagerTest is Test { } /*////////////////////////////////////////////////////////////// - MOCK CONTRACTS + MOCK CONTRACTS //////////////////////////////////////////////////////////////*/ - contract MockToken is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) {} @@ -4019,7 +4118,6 @@ contract MockSlashingManager { receive() external payable {} - /// @dev Also configures preview data from the same arrays (min-length safe). function setSlashResult(address[] memory assets, uint256[] memory amounts) external { _assets = assets; _amounts = amounts; @@ -4033,8 +4131,6 @@ contract MockSlashingManager { _token2ToTransfer = token; } - /// @dev Returns preview using the same assets/amounts set via setSlashResult. - /// Uses min(assets.length, amounts.length) to stay safe with mismatched test data. function previewSlashing( uint96, address @@ -4059,7 +4155,7 @@ contract MockSlashingManager { uint96 committeeId, address operator, ISlashingManager.VaultSlash[] memory vaultSlashes, - bytes32 /*taskId*/ + bytes32 ) external returns (address[] memory, uint256[] memory) @@ -4101,7 +4197,7 @@ contract MockSwapper { uint256 private _swapOutput; address private _payoutToken; - address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // WETH placeholder + address public nativeWrapper = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); receive() external payable {} @@ -4113,7 +4209,6 @@ contract MockSwapper { _payoutToken = token; } - /// @dev Returns amountIn (1:1 ratio) so _computeVaultSlashes proportional math works in tests. function quoteSwap(address, address, uint256 amountIn) external pure returns (uint256) { return amountIn; } @@ -4147,17 +4242,13 @@ contract MockPolicy { } } -/** - * @dev Configurable oracle price feed mock. - * - All tokens default to a 1:1 price (price = 1e18). - * - `setFeedMissing(token)` makes hasPriceFeed return false and getUSDValue revert. - * - `setShouldRevert(true)` makes every external call revert (staleness simulation). - * - `setPrice(token, price)` overrides the per-token price (in 1e18 scale). - */ contract MockOraclePriceFeed { mapping(address => uint256) private _pricePerToken; mapping(address => bool) private _feedMissing; bool private _shouldRevert; + mapping(address => bool) private _usdValueZero; + bool private _getTokenAmountShouldRevert; + mapping(address => bool) private _tokenAmountZero; function setPrice(address token, uint256 price) external { _pricePerToken[token] = price; @@ -4171,6 +4262,18 @@ contract MockOraclePriceFeed { _shouldRevert = shouldRevert_; } + function setUSDValueZero(address token) external { + _usdValueZero[token] = true; + } + + function setGetTokenAmountShouldRevert(bool value) external { + _getTokenAmountShouldRevert = value; + } + + function setTokenAmountZero(address token) external { + _tokenAmountZero[token] = true; + } + function hasPriceFeed(address token) external view returns (bool) { return !_feedMissing[token]; } @@ -4178,31 +4281,26 @@ contract MockOraclePriceFeed { function getUSDValue(address token, uint256 amount) external view returns (uint256) { require(!_shouldRevert, "MockOraclePriceFeed: stale"); require(!_feedMissing[token], "MockOraclePriceFeed: no feed"); + if (_usdValueZero[token]) return 0; uint256 price = _pricePerToken[token]; if (price == 0) { - return amount; // 1:1 default + return amount; } return (amount * price) / 1e18; } function getTokenAmount(address token, uint256 usdValue) external view returns (uint256) { - require(!_shouldRevert, "MockOraclePriceFeed: stale"); + require(!_getTokenAmountShouldRevert, "MockOraclePriceFeed: getTokenAmount reverts"); require(!_feedMissing[token], "MockOraclePriceFeed: no feed"); + if (_tokenAmountZero[token]) return 0; uint256 price = _pricePerToken[token]; if (price == 0) { - return usdValue; // 1:1 default + return usdValue; } return (usdValue * 1e18) / price; } } -/** - * @dev Multi-token swapper mock supporting separate quoteSwap and executeSwap outputs per token. - * - `setSwapOutput(token, amt)`: sets both quote and execution output (default: amountIn 1:1). - * - `setQuoteOutput(token, amt)`: overrides only the quoteSwap output independently of executeSwap. - * This allows tests to have quoteSwap return 1:1 (passing oracle deviation checks) while executeSwap - * returns a different amount (simulating actual swap price impact). - */ contract MockMultiSwapper { mapping(address => uint256) private _swapOutputs; mapping(address => uint256) private _quoteOutputs; @@ -4213,8 +4311,6 @@ contract MockMultiSwapper { _swapOutputs[tokenIn] = output; } - /// @dev Separate quoteSwap amount — allows passing oracle validation (1:1 with oracle) - /// while executeSwap returns a different amount (simulating price impact). function setQuoteOutput(address tokenIn, uint256 output) external { _quoteOutputs[tokenIn] = output; } @@ -4223,12 +4319,10 @@ contract MockMultiSwapper { _payoutToken = token; } - /// @dev Returns _quoteOutputs if set, otherwise amountIn (1:1 default). - /// The 1:1 default ensures oracle deviation gate passes in tests that only configure executeSwap output. function quoteSwap(address tokenIn, address, uint256 amountIn) external view returns (uint256) { uint256 q = _quoteOutputs[tokenIn]; if (q > 0) return q; - return amountIn; // 1:1 default: consistent with oracle 1:1 mock + return amountIn; } function executeSwap(ISwapper.SwapParams calldata params) external returns (uint256) { @@ -4255,7 +4349,6 @@ contract MockCoverPool { } } -/// @dev CoverPool mock whose owner can be changed after deployment, simulating a pool ownership transfer. contract MutableOwnerCoverPool { address private _owner; @@ -4272,11 +4365,6 @@ contract MutableOwnerCoverPool { } } -/** - * @dev Swapper mock that makes quoteSwap revert or return 0 for configured tokens, simulating - * collateral tokens with no quote route. executeSwap also reverts for those tokens so that - * any attempt to actually swap them (which should never happen after the fix) is caught. - */ contract MockSwapperWithUnquotable { mapping(address => bool) private _revertOnQuote; mapping(address => bool) private _zeroOnQuote; @@ -4321,11 +4409,6 @@ contract MockSwapperWithUnquotable { } } -/** - * @dev SlashingManager mock where executeSlashing honours the VaultSlash.amount values passed - * to it (rather than returning a pre-set constant). This lets tests verify that unquotable - * vaults receive amount=0 and are therefore not transferred. - */ contract MockSlashingManagerRespectingSlash { address[] private _previewVaults; address[] private _previewTokens; @@ -4423,11 +4506,10 @@ contract MockNativeMultiSwapper { _payoutToken = token; } - /// @dev Returns _quoteOutputs if set, otherwise amountIn (1:1 default). function quoteSwap(address tokenIn, address, uint256 amountIn) external view returns (uint256) { uint256 q = _quoteOutputs[tokenIn]; if (q > 0) return q; - return amountIn; // 1:1 default + return amountIn; } function executeSwap(ISwapper.SwapParams calldata params) external payable returns (uint256) { @@ -4445,10 +4527,6 @@ contract MockNativeMultiSwapper { } } -/** - * @dev SlashingManager mock that records the VaultSlash array and reverts if any entry has amount == 0. - * Used to assert that _computeVaultSlashes compacts zero-amount entries before calling executeSlashing. - */ contract MockSlashingManagerAssertingNoZeroAmounts { address[] private _previewVaults; address[] private _previewTokens; @@ -4507,11 +4585,6 @@ contract MockSlashingManagerAssertingNoZeroAmounts { } } -/** - * @dev Minimal PremiumManager mock that implements distributeSurplusAsRewards() by pulling tokens from the - * caller via safeTransferFrom, mirroring the real PremiumManager behaviour. Tracks the - * last call parameters and accumulates received tokens for assertion. - */ contract MockPremiumManagerForClaims { using SafeERC20 for IERC20; @@ -4535,26 +4608,12 @@ contract MockPremiumManagerForClaims { } } -/** - * @dev PremiumManager mock that always reverts on distributeSurplusAsRewards(), simulating a Core-side - * failure (e.g. duplicate taskId, paused state, or an upgrade regression). - * Used to verify that _distributeSurplus's try/catch allows the claim to complete. - */ contract MockPremiumManagerReverting { function distributeSurplusAsRewards(uint96, address, uint256, IERC20, bytes32) external pure { revert("MockPremiumManagerReverting: always reverts"); } } -/** - * @dev Swapper mock where quoteSwap applies a per-token integer divisor, simulating realistic - * DEX price ratios. Tokens without an explicit divisor default to 1:1. - * - * Purpose: reproduce the QuoteUnavailable-blocking-claim bug where a cross-token vault - * passes the first-pass inclusion check (quoteSwap(token, payout, fullStake) > 0) but - * its computed proportional baseSlash rounds to 0, which under the old code was forced - * to 1, causing quoteSwap(token, payout, 1) to return 0 and revert. - */ contract MockSwapperWithPerTokenDivisor { address private constant _NATIVE_ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; @@ -4575,7 +4634,6 @@ contract MockSwapperWithPerTokenDivisor { _swapOutput = output; } - /// @dev Returns amountIn / divisor (flooring), or amountIn when no divisor is set (1:1). function quoteSwap(address tokenIn, address, uint256 amountIn) external view returns (uint256) { uint256 d = _divisors[tokenIn]; return d > 0 ? amountIn / d : amountIn;