diff --git a/src/OracleHub.sol b/src/OracleHub.sol index daa63e4..abef960 100644 --- a/src/OracleHub.sol +++ b/src/OracleHub.sol @@ -105,16 +105,16 @@ contract OracleHub is IPriceSource, IRateOracle, Ownable { return snapshotEthUsdWad; // degraded: last-good so floor defense continues } - /// @notice wstETH marked at the LOWER of exchange-rate and pool-spot - /// valuations whenever they disagree beyond the basis limit. + /// @notice wstETH marked at the Lido exchange rate (stEthPerToken x + /// ETH/USD), NOT the DEX spot (audit H5). The exchange rate is the + /// fundamental redemption value and, unlike a UniV3 slot0 read, is + /// not flash-loan manipulable, so it cannot be used to skew share + /// pricing at settlement. This is the standard wstETH-collateral + /// marking (Aave/Morpho). The pool spot is kept only as a depeg + /// gate on BUYING more wstETH (wstethBuyAllowed), where a pushed + /// spot is fail-safe: it can only block a buy, never inflate value. function wstethUsdWad() external view returns (uint256) { - uint256 eth = ethUsdWad(); - uint256 rateBased = wsteth.stEthPerToken().mulWad(eth); - uint256 poolBased = _poolWethPerWsteth().mulWad(eth); - if (_basisBps(rateBased, poolBased) > maxBasisBps) { - return FixedPointMathLib.min(rateBased, poolBased); - } - return rateBased; + return wsteth.stEthPerToken().mulWad(ethUsdWad()); } function wstethBuyAllowed() external view returns (bool) { diff --git a/test/OracleHub.t.sol b/test/OracleHub.t.sol index addf1a3..5cfac58 100644 --- a/test/OracleHub.t.sol +++ b/test/OracleHub.t.sol @@ -97,16 +97,25 @@ contract OracleHubTest is Test { hub.refresh(); } - function test_basisBreach_marksLower_blocksBuys() public { - // pool prints wstETH 5% below the exchange rate (depeg scenario) - pool.setRatioWad(1.14e18); - assertApproxEqRel(hub.wstethUsdWad(), 2280e18, 1e6); // marked at pool (lower) + function test_h5_basisBreach_marksAtRate_blocksBuys() public { + // H5: the VALUE mark is always the exchange rate, never the pool spot, + // so a manipulated/depegged pool cannot skew NAV/share pricing. A basis + // breach only flips the buy gate (fail-safe, blocks buying more wstETH). + pool.setRatioWad(1.14e18); // pool 5% below the exchange rate (depeg) + assertApproxEqRel(hub.wstethUsdWad(), 2400e18, 1e6); // still exchange-rate + assertFalse(hub.wstethBuyAllowed()); // but buys are blocked + + pool.setRatioWad(1.32e18); // pool above the exchange rate + assertApproxEqRel(hub.wstethUsdWad(), 2400e18, 1e6); // still exchange-rate assertFalse(hub.wstethBuyAllowed()); + } - // exchange rate lower than pool: still marks the lower one - pool.setRatioWad(1.32e18); - assertApproxEqRel(hub.wstethUsdWad(), 2400e18, 1e6); // rate-based is lower - assertFalse(hub.wstethBuyAllowed()); + function test_h5_poolPushDownCannotLowerTheMark() public { + // the attack: push the pool spot far down to depress the wstETH mark + // and mint extra shares cheaply. Under the fix the mark does not move. + uint256 markBefore = hub.wstethUsdWad(); + pool.setRatioWad(0.5e18); // extreme flash-manipulated spot + assertEq(hub.wstethUsdWad(), markBefore); // unaffected } function test_smallBasis_usesExchangeRate() public {