Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/OracleHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
25 changes: 17 additions & 8 deletions test/OracleHub.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down