// 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 ICWMultiTokenBridgeL1Accounting { 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 IOwnableLike { function owner() external view returns (address); } interface IStablecoinReserveVaultLike { function compliantUSDT() external view returns (address); function compliantUSDC() external view returns (address); function getBackingRatio(address token) external view returns ( uint256 reserveBalance, uint256 tokenSupply, uint256 backingRatio ); function checkReserveAdequacy() external view returns (bool usdtAdequate, bool usdcAdequate); } /** * @title CWReserveVerifier * @notice Verifies bridge escrow and canonical reserve backing before allowing new cW minting. * @dev Intended for cUSDC/cUSDT -> cWUSDC/cWUSDT hard-peg flows on Chain 138. */ contract CWReserveVerifier is AccessControl, ICWReserveVerifier { bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); uint256 public constant HARD_THRESHOLD_BPS = 10000; 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; } ICWMultiTokenBridgeL1Accounting public bridge; IStablecoinReserveVaultLike public stablecoinReserveVault; IReserveSystem public reserveSystem; mapping(address => TokenConfig) public tokenConfigs; event BridgeUpdated(address indexed newBridge); event StablecoinReserveVaultUpdated(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 UnsupportedCanonicalToken(); error BridgeTokenPaused(); error EscrowInvariantViolation(); error ChainOutstandingCapExceeded(); error TokenOwnerMismatch(); error VaultBackingInsufficient(); error ReserveSystemBackingInsufficient(); constructor( address admin, address bridge_, address stablecoinReserveVault_, address reserveSystem_ ) { if (admin == address(0) || bridge_ == address(0)) { revert ZeroAddress(); } _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(OPERATOR_ROLE, admin); bridge = ICWMultiTokenBridgeL1Accounting(bridge_); stablecoinReserveVault = IStablecoinReserveVaultLike(stablecoinReserveVault_); reserveSystem = IReserveSystem(reserveSystem_); } function setBridge(address bridge_) external onlyRole(DEFAULT_ADMIN_ROLE) { if (bridge_ == address(0)) { revert ZeroAddress(); } bridge = ICWMultiTokenBridgeL1Accounting(bridge_); emit BridgeUpdated(bridge_); } function setStablecoinReserveVault(address stablecoinReserveVault_) external onlyRole(DEFAULT_ADMIN_ROLE) { stablecoinReserveVault = IStablecoinReserveVaultLike(stablecoinReserveVault_); emit StablecoinReserveVaultUpdated(stablecoinReserveVault_); } 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) && address(stablecoinReserveVault) == address(0)) { revert VaultRequired(); } if (requireReserveSystemBalance && address(reserveSystem) == address(0)) { revert ReserveSystemRequired(); } if (requireReserveSystemBalance && reserveAsset == address(0)) { revert ZeroAddress(); } 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.requireVaultBacking || config.requireTokenOwnerMatchVault) { address vaultAddress = address(stablecoinReserveVault); if (config.requireTokenOwnerMatchVault) { try IOwnableLike(canonicalToken).owner() returns (address tokenOwner) { status.tokenOwnerMatchesVault = tokenOwner == vaultAddress; } catch { status.tokenOwnerMatchesVault = false; } } if (config.requireVaultBacking) { bool tokenTrackedByVault; bool tokenAdequate; try stablecoinReserveVault.compliantUSDT() returns (address compliantUSDT) { if (canonicalToken == compliantUSDT) { tokenTrackedByVault = true; try stablecoinReserveVault.checkReserveAdequacy() returns (bool usdtAdequate, bool) { tokenAdequate = usdtAdequate; } catch {} } } catch {} if (!tokenTrackedByVault) { try stablecoinReserveVault.compliantUSDC() returns (address compliantUSDC) { if (canonicalToken == compliantUSDC) { tokenTrackedByVault = true; try stablecoinReserveVault.checkReserveAdequacy() returns (bool, bool usdcAdequate) { tokenAdequate = usdcAdequate; } catch {} } } catch {} } if (tokenTrackedByVault) { try stablecoinReserveVault.getBackingRatio(canonicalToken) returns ( uint256 reserveBalance, uint256, uint256 backingRatio ) { status.vaultReserveBalance = reserveBalance; status.vaultBackingRatio = backingRatio; status.vaultAdequate = tokenAdequate && backingRatio >= HARD_THRESHOLD_BPS && reserveBalance >= status.canonicalTotalSupply; } catch { status.vaultAdequate = false; } } else { status.vaultAdequate = false; } } } status.passes = status.supportedCanonicalToken && !status.bridgePaused && status.escrowSufficient && status.chainCapSufficient && status.tokenOwnerMatchesVault && status.vaultAdequate && status.reserveSystemAdequate; } }