// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "../reserve/IReserveSystem.sol"; interface ICWNavBridgeL1 { function lockedBalance(address token) external view returns (uint256); function totalOutstanding(address token) external view returns (uint256); } /** * @title CWNavOracle * @notice Publishes reserve-backed NAV for canonical c* / global cW supply pairs. * @dev Global cW supply is aggregated off-chain across mesh chains and pushed by OPERATOR_ROLE. * On-chain locked/outstanding reads come from CWMultiTokenBridgeL1. */ contract CWNavOracle is AccessControl { bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); uint256 public constant BPS = 10_000; uint256 public constant NAV_SCALE = 1e18; struct TokenConfig { bool enabled; address reserveAsset; } struct NavSnapshot { uint256 totalLockedAssets; uint256 totalReserveAssets; uint256 totalCwSupply; uint256 backingRatioBps; uint256 navPerShare; uint256 chain138CollateralBps; uint256 treasuryReservesBps; uint256 protocolFeesBps; bytes32 attestationHash; uint256 updatedAt; } ICWNavBridgeL1 public bridge; IReserveSystem public reserveSystem; mapping(address => TokenConfig) public tokenConfigs; mapping(address => uint256) public globalCwSupply; mapping(address => NavSnapshot) public latestSnapshot; event BridgeUpdated(address indexed newBridge); event ReserveSystemUpdated(address indexed newReserveSystem); event TokenConfigured(address indexed canonicalToken, address indexed reserveAsset); event TokenDisabled(address indexed canonicalToken); event NavPublished(address indexed canonicalToken, uint256 backingRatioBps, uint256 navPerShare, bytes32 attestationHash); error ZeroAddress(); error TokenNotConfigured(); constructor(address admin, address bridge_, address reserveSystem_) { if (admin == address(0) || bridge_ == address(0)) { revert ZeroAddress(); } _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(OPERATOR_ROLE, admin); bridge = ICWNavBridgeL1(bridge_); reserveSystem = IReserveSystem(reserveSystem_); } function setBridge(address bridge_) external onlyRole(DEFAULT_ADMIN_ROLE) { if (bridge_ == address(0)) { revert ZeroAddress(); } bridge = ICWNavBridgeL1(bridge_); emit BridgeUpdated(bridge_); } function setReserveSystem(address reserveSystem_) external onlyRole(DEFAULT_ADMIN_ROLE) { reserveSystem = IReserveSystem(reserveSystem_); emit ReserveSystemUpdated(reserveSystem_); } function configureToken(address canonicalToken, address reserveAsset) external onlyRole(OPERATOR_ROLE) { if (canonicalToken == address(0)) { revert ZeroAddress(); } tokenConfigs[canonicalToken] = TokenConfig({enabled: true, reserveAsset: reserveAsset}); emit TokenConfigured(canonicalToken, reserveAsset); } function disableToken(address canonicalToken) external onlyRole(OPERATOR_ROLE) { delete tokenConfigs[canonicalToken]; emit TokenDisabled(canonicalToken); } function publishNav( address canonicalToken, uint256 globalCwSupplyAmount, uint256 chain138CollateralBps, uint256 treasuryReservesBps, uint256 protocolFeesBps, bytes32 attestationHash ) external onlyRole(OPERATOR_ROLE) { TokenConfig memory config = tokenConfigs[canonicalToken]; if (!config.enabled) { revert TokenNotConfigured(); } require(chain138CollateralBps + treasuryReservesBps + protocolFeesBps == BPS, "CWNavOracle: bad decomposition"); globalCwSupply[canonicalToken] = globalCwSupplyAmount; uint256 locked = bridge.lockedBalance(canonicalToken); uint256 reserveBalance = address(reserveSystem) == address(0) || config.reserveAsset == address(0) ? 0 : reserveSystem.getReserveBalance(config.reserveAsset); uint256 totalAssets = locked + reserveBalance; uint256 backingRatioBps = globalCwSupplyAmount == 0 ? 0 : (totalAssets * BPS) / globalCwSupplyAmount; uint256 navPerShareValue = globalCwSupplyAmount == 0 ? 0 : (totalAssets * NAV_SCALE) / globalCwSupplyAmount; NavSnapshot memory snap = NavSnapshot({ totalLockedAssets: locked, totalReserveAssets: reserveBalance, totalCwSupply: globalCwSupplyAmount, backingRatioBps: backingRatioBps, navPerShare: navPerShareValue, chain138CollateralBps: chain138CollateralBps, treasuryReservesBps: treasuryReservesBps, protocolFeesBps: protocolFeesBps, attestationHash: attestationHash, updatedAt: block.timestamp }); latestSnapshot[canonicalToken] = snap; emit NavPublished(canonicalToken, backingRatioBps, navPerShareValue, attestationHash); } function totalLockedAssets(address canonicalToken) external view returns (uint256) { return bridge.lockedBalance(canonicalToken); } function totalReserveAssets(address canonicalToken) external view returns (uint256) { TokenConfig memory config = tokenConfigs[canonicalToken]; if (!config.enabled || address(reserveSystem) == address(0) || config.reserveAsset == address(0)) { return 0; } return reserveSystem.getReserveBalance(config.reserveAsset); } function totalCWUSupply(address canonicalToken) external view returns (uint256) { return globalCwSupply[canonicalToken]; } function backingRatio(address canonicalToken) external view returns (uint256 backingRatioBps) { return latestSnapshot[canonicalToken].backingRatioBps; } function navPerShare(address canonicalToken) external view returns (uint256) { return latestSnapshot[canonicalToken].navPerShare; } function getNavSnapshot(address canonicalToken) external view returns (NavSnapshot memory) { return latestSnapshot[canonicalToken]; } }