163 lines
6.2 KiB
Solidity
163 lines
6.2 KiB
Solidity
// 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];
|
|
}
|
|
}
|