// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; /** * @title WLPNAVOracle * @notice Chainlink-style read interface for wLP / vault share price in USD (8 decimals) for lending adapters. * @dev Keeper updates `latestAnswer` + `updatedAt`. Consumers must check `isStale()` before liquidations. */ contract WLPNAVOracle is AccessControl { bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE"); /// @notice Price with 8 decimals (Chainlink USD convention). int256 public latestAnswer; uint256 public updatedAt; uint256 public heartbeat; bool public circuitBreaker; event AnswerUpdated(int256 indexed answer, uint256 updatedAt, address indexed keeper); event HeartbeatUpdated(uint256 heartbeat); event CircuitBreakerSet(bool tripped); constructor(address admin, uint256 heartbeat_) { require(admin != address(0), "NAV: zero admin"); heartbeat = heartbeat_; _grantRole(DEFAULT_ADMIN_ROLE, admin); } function setHeartbeat(uint256 h) external onlyRole(DEFAULT_ADMIN_ROLE) { heartbeat = h; emit HeartbeatUpdated(h); } function setCircuitBreaker(bool on) external onlyRole(DEFAULT_ADMIN_ROLE) { circuitBreaker = on; emit CircuitBreakerSet(on); } function submitAnswer(int256 answer) external onlyRole(KEEPER_ROLE) { require(!circuitBreaker, "NAV: breaker"); latestAnswer = answer; updatedAt = block.timestamp; emit AnswerUpdated(answer, updatedAt, msg.sender); } function isStale() external view returns (bool) { if (circuitBreaker) return true; if (heartbeat == 0) return false; return block.timestamp > updatedAt + heartbeat; } }