// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../reserve/IReserveSystem.sol"; import "./ICWReserveVerifier.sol"; interface ICWMultiTokenBridgeL1AccountingV2 { function supportedCanonicalToken(address token) external view returns (bool); function paused(address token) external view returns (bool); function lockedBalance(address token) external view returns (uint256); function totalOutstanding(address token) external view returns (uint256); function outstandingMinted(address token, uint64 destinationChainSelector) external view returns (uint256); function maxOutstanding(address token, uint64 destinationChainSelector) external view returns (uint256); } interface IOwnableLikeV2 { function owner() external view returns (address); } interface IBalanceOfAsset { function balanceOf(address account) external view returns (uint256); } /** * @title CWAssetReserveVerifier * @notice Generic reserve verifier for canonical c* -> mirrored cW* bridge lanes. * @dev Uses bridge accounting plus optional vault and reserve-system checks so one verifier * can cover stable, monetary-unit, and gas-native families behind a single L1 bridge. */ contract CWAssetReserveVerifier is AccessControl, ICWReserveVerifier { bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); uint256 public constant HARD_THRESHOLD_BPS = 10_000; struct TokenConfig { bool enabled; address reserveAsset; bool requireVaultBacking; bool requireReserveSystemBalance; bool requireTokenOwnerMatchVault; } struct VerificationStatus { bool supportedCanonicalToken; bool bridgePaused; bool escrowSufficient; bool chainCapSufficient; bool reserveSystemAdequate; bool vaultAdequate; bool tokenOwnerMatchesVault; bool passes; uint256 lockedBalanceAmount; uint256 totalOutstandingAmount; uint256 chainOutstandingAmount; uint256 maxOutstandingAmount; uint256 canonicalTotalSupply; uint256 reserveSystemBalance; uint256 vaultReserveBalance; uint256 vaultBackingRatio; } ICWMultiTokenBridgeL1AccountingV2 public bridge; address public assetVault; IReserveSystem public reserveSystem; mapping(address => TokenConfig) public tokenConfigs; event BridgeUpdated(address indexed newBridge); event AssetVaultUpdated(address indexed newVault); event ReserveSystemUpdated(address indexed newReserveSystem); event TokenConfigured( address indexed canonicalToken, address indexed reserveAsset, bool requireVaultBacking, bool requireReserveSystemBalance, bool requireTokenOwnerMatchVault ); event TokenDisabled(address indexed canonicalToken); error ZeroAddress(); error TokenNotConfigured(); error VaultRequired(); error ReserveSystemRequired(); error ReserveAssetRequired(); error UnsupportedCanonicalToken(); error BridgeTokenPaused(); error EscrowInvariantViolation(); error ChainOutstandingCapExceeded(); error TokenOwnerMismatch(); error VaultBackingInsufficient(); error ReserveSystemBackingInsufficient(); constructor( address admin, address bridge_, address assetVault_, address reserveSystem_ ) { if (admin == address(0) || bridge_ == address(0)) revert ZeroAddress(); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(OPERATOR_ROLE, admin); bridge = ICWMultiTokenBridgeL1AccountingV2(bridge_); assetVault = assetVault_; reserveSystem = IReserveSystem(reserveSystem_); } function setBridge(address bridge_) external onlyRole(DEFAULT_ADMIN_ROLE) { if (bridge_ == address(0)) revert ZeroAddress(); bridge = ICWMultiTokenBridgeL1AccountingV2(bridge_); emit BridgeUpdated(bridge_); } function setAssetVault(address assetVault_) external onlyRole(DEFAULT_ADMIN_ROLE) { assetVault = assetVault_; emit AssetVaultUpdated(assetVault_); } function setReserveSystem(address reserveSystem_) external onlyRole(DEFAULT_ADMIN_ROLE) { reserveSystem = IReserveSystem(reserveSystem_); emit ReserveSystemUpdated(reserveSystem_); } function configureToken( address canonicalToken, address reserveAsset, bool requireVaultBacking, bool requireReserveSystemBalance, bool requireTokenOwnerMatchVault ) external onlyRole(OPERATOR_ROLE) { if (canonicalToken == address(0)) revert ZeroAddress(); if ((requireVaultBacking || requireTokenOwnerMatchVault) && assetVault == address(0)) revert VaultRequired(); if (requireReserveSystemBalance && address(reserveSystem) == address(0)) revert ReserveSystemRequired(); if ((requireVaultBacking || requireReserveSystemBalance) && reserveAsset == address(0)) revert ReserveAssetRequired(); tokenConfigs[canonicalToken] = TokenConfig({ enabled: true, reserveAsset: reserveAsset, requireVaultBacking: requireVaultBacking, requireReserveSystemBalance: requireReserveSystemBalance, requireTokenOwnerMatchVault: requireTokenOwnerMatchVault }); emit TokenConfigured( canonicalToken, reserveAsset, requireVaultBacking, requireReserveSystemBalance, requireTokenOwnerMatchVault ); } function disableToken(address canonicalToken) external onlyRole(OPERATOR_ROLE) { delete tokenConfigs[canonicalToken]; emit TokenDisabled(canonicalToken); } function verifyLock( address canonicalToken, uint64 destinationChainSelector, uint256 ) external view override returns (bool verified) { TokenConfig memory config = tokenConfigs[canonicalToken]; if (!config.enabled) revert TokenNotConfigured(); VerificationStatus memory status = _buildStatus(canonicalToken, destinationChainSelector, config); if (!status.supportedCanonicalToken) revert UnsupportedCanonicalToken(); if (status.bridgePaused) revert BridgeTokenPaused(); if (!status.escrowSufficient) revert EscrowInvariantViolation(); if (!status.chainCapSufficient) revert ChainOutstandingCapExceeded(); if (config.requireTokenOwnerMatchVault && !status.tokenOwnerMatchesVault) revert TokenOwnerMismatch(); if (config.requireVaultBacking && !status.vaultAdequate) revert VaultBackingInsufficient(); if (config.requireReserveSystemBalance && !status.reserveSystemAdequate) revert ReserveSystemBackingInsufficient(); return true; } function getVerificationStatus( address canonicalToken, uint64 destinationChainSelector ) external view returns (VerificationStatus memory status) { TokenConfig memory config = tokenConfigs[canonicalToken]; if (!config.enabled) revert TokenNotConfigured(); return _buildStatus(canonicalToken, destinationChainSelector, config); } function _buildStatus( address canonicalToken, uint64 destinationChainSelector, TokenConfig memory config ) internal view returns (VerificationStatus memory status) { status.supportedCanonicalToken = bridge.supportedCanonicalToken(canonicalToken); status.bridgePaused = bridge.paused(canonicalToken); status.lockedBalanceAmount = bridge.lockedBalance(canonicalToken); status.totalOutstandingAmount = bridge.totalOutstanding(canonicalToken); status.chainOutstandingAmount = bridge.outstandingMinted(canonicalToken, destinationChainSelector); status.maxOutstandingAmount = bridge.maxOutstanding(canonicalToken, destinationChainSelector); status.escrowSufficient = status.lockedBalanceAmount >= status.totalOutstandingAmount; status.chainCapSufficient = status.maxOutstandingAmount == 0 || status.chainOutstandingAmount <= status.maxOutstandingAmount; status.canonicalTotalSupply = IERC20(canonicalToken).totalSupply(); status.reserveSystemAdequate = !config.requireReserveSystemBalance; if (config.requireReserveSystemBalance) { status.reserveSystemBalance = reserveSystem.getReserveBalance(config.reserveAsset); status.reserveSystemAdequate = status.reserveSystemBalance >= status.canonicalTotalSupply; } status.vaultAdequate = !config.requireVaultBacking; status.tokenOwnerMatchesVault = !config.requireTokenOwnerMatchVault; if (config.requireTokenOwnerMatchVault) { try IOwnableLikeV2(canonicalToken).owner() returns (address tokenOwner) { status.tokenOwnerMatchesVault = tokenOwner == assetVault; } catch { status.tokenOwnerMatchesVault = false; } } if (config.requireVaultBacking) { try IBalanceOfAsset(config.reserveAsset).balanceOf(assetVault) returns (uint256 reserveBalance) { status.vaultReserveBalance = reserveBalance; if (status.canonicalTotalSupply > 0) { status.vaultBackingRatio = (reserveBalance * HARD_THRESHOLD_BPS) / status.canonicalTotalSupply; } else { status.vaultBackingRatio = HARD_THRESHOLD_BPS; } status.vaultAdequate = status.vaultBackingRatio >= HARD_THRESHOLD_BPS && reserveBalance >= status.canonicalTotalSupply; } catch { status.vaultAdequate = false; } } status.passes = status.supportedCanonicalToken && !status.bridgePaused && status.escrowSufficient && status.chainCapSufficient && status.tokenOwnerMatchesVault && status.vaultAdequate && status.reserveSystemAdequate; } }