// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; /** * @title BridgeRegistry * @notice Registry for bridge configuration: destinations, tokens, fees, and routing */ contract BridgeRegistry is AccessControl, Pausable { bytes32 public constant REGISTRAR_ROLE = keccak256("REGISTRAR_ROLE"); struct Destination { uint256 chainId; // 0 for non-EVM string chainName; bool enabled; uint256 minFinalityBlocks; uint256 timeoutSeconds; uint256 baseFee; // Base fee in basis points (10000 = 100%) address feeRecipient; } struct TokenConfig { address tokenAddress; bool allowed; uint256 minAmount; uint256 maxAmount; uint256[] allowedDestinations; // Chain IDs or 0 for non-EVM uint8 riskLevel; // 0-255, higher = riskier uint256 bridgeFeeBps; // Bridge fee in basis points } struct RouteHealth { uint256 successCount; uint256 failureCount; uint256 lastUpdate; uint256 avgSettlementTime; // In seconds } mapping(uint256 => Destination) public destinations; // chainId -> Destination mapping(address => TokenConfig) public tokenConfigs; mapping(uint256 => mapping(address => RouteHealth)) public routeHealth; // chainId -> token -> health mapping(address => bool) public allowedTokens; uint256[] public destinationChainIds; address[] public registeredTokens; event DestinationRegistered( uint256 indexed chainId, string chainName, uint256 minFinalityBlocks, uint256 timeoutSeconds ); event DestinationUpdated(uint256 indexed chainId, bool enabled); event TokenRegistered( address indexed token, uint256 minAmount, uint256 maxAmount, uint8 riskLevel ); event TokenUpdated(address indexed token, bool allowed); event RouteHealthUpdated( uint256 indexed chainId, address indexed token, bool success, uint256 settlementTime ); error DestinationNotFound(); error TokenNotAllowed(); error InvalidAmount(); error InvalidDestination(); error InvalidFee(); constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(REGISTRAR_ROLE, admin); } /** * @notice Register a new destination chain * @param chainId Chain ID (0 for non-EVM like XRPL) * @param chainName Human-readable chain name * @param minFinalityBlocks Minimum blocks/ledgers for finality * @param timeoutSeconds Timeout for refund eligibility * @param baseFee Base fee in basis points * @param feeRecipient Address to receive fees */ function registerDestination( uint256 chainId, string calldata chainName, uint256 minFinalityBlocks, uint256 timeoutSeconds, uint256 baseFee, address feeRecipient ) external onlyRole(REGISTRAR_ROLE) { if (baseFee > 10000) revert InvalidFee(); // Max 100% destinations[chainId] = Destination({ chainId: chainId, chainName: chainName, enabled: true, minFinalityBlocks: minFinalityBlocks, timeoutSeconds: timeoutSeconds, baseFee: baseFee, feeRecipient: feeRecipient }); // Add to list if not already present bool exists = false; for (uint256 i = 0; i < destinationChainIds.length; i++) { if (destinationChainIds[i] == chainId) { exists = true; break; } } if (!exists) { destinationChainIds.push(chainId); } emit DestinationRegistered(chainId, chainName, minFinalityBlocks, timeoutSeconds); } /** * @notice Update destination enabled status * @param chainId Chain ID * @param enabled Enabled status */ function updateDestination( uint256 chainId, bool enabled ) external onlyRole(REGISTRAR_ROLE) { if (destinations[chainId].chainId == 0 && chainId != 0) revert DestinationNotFound(); destinations[chainId].enabled = enabled; emit DestinationUpdated(chainId, enabled); } /** * @notice Register a token for bridging * @param token Token address * @param minAmount Minimum bridge amount * @param maxAmount Maximum bridge amount * @param allowedDestinations Array of allowed destination chain IDs * @param riskLevel Risk level (0-255) * @param bridgeFeeBps Bridge fee in basis points */ function registerToken( address token, uint256 minAmount, uint256 maxAmount, uint256[] calldata allowedDestinations, uint8 riskLevel, uint256 bridgeFeeBps ) external onlyRole(REGISTRAR_ROLE) { if (bridgeFeeBps > 10000) revert InvalidFee(); tokenConfigs[token] = TokenConfig({ tokenAddress: token, allowed: true, minAmount: minAmount, maxAmount: maxAmount, allowedDestinations: allowedDestinations, riskLevel: riskLevel, bridgeFeeBps: bridgeFeeBps }); allowedTokens[token] = true; // Add to list if not already present bool exists = false; for (uint256 i = 0; i < registeredTokens.length; i++) { if (registeredTokens[i] == token) { exists = true; break; } } if (!exists) { registeredTokens.push(token); } emit TokenRegistered(token, minAmount, maxAmount, riskLevel); } /** * @notice Update token allowed status * @param token Token address * @param allowed Allowed status */ function updateToken(address token, bool allowed) external onlyRole(REGISTRAR_ROLE) { if (tokenConfigs[token].tokenAddress == address(0)) revert TokenNotAllowed(); tokenConfigs[token].allowed = allowed; allowedTokens[token] = allowed; emit TokenUpdated(token, allowed); } /** * @notice Update route health metrics * @param chainId Destination chain ID * @param token Token address * @param success Whether the route succeeded * @param settlementTime Settlement time in seconds */ function updateRouteHealth( uint256 chainId, address token, bool success, uint256 settlementTime ) external onlyRole(REGISTRAR_ROLE) { RouteHealth storage health = routeHealth[chainId][token]; if (success) { health.successCount++; // Update average settlement time (simple moving average) if (health.successCount == 1) { health.avgSettlementTime = settlementTime; } else { health.avgSettlementTime = (health.avgSettlementTime * (health.successCount - 1) + settlementTime) / health.successCount; } } else { health.failureCount++; } health.lastUpdate = block.timestamp; emit RouteHealthUpdated(chainId, token, success, settlementTime); } /** * @notice Validate bridge request * @param token Token address (address(0) for native) * @param amount Amount to bridge * @param destinationChainId Destination chain ID * @return isValid Whether request is valid * @return fee Fee amount */ function validateBridgeRequest( address token, uint256 amount, uint256 destinationChainId ) external view returns (bool isValid, uint256 fee) { // Check destination exists and is enabled Destination memory dest = destinations[destinationChainId]; if (dest.chainId == 0 && destinationChainId != 0) { return (false, 0); } if (!dest.enabled) { return (false, 0); } // For native ETH, allow if destination is enabled if (token == address(0)) { fee = (amount * dest.baseFee) / 10000; return (true, fee); } // Check token is registered and allowed TokenConfig memory config = tokenConfigs[token]; if (!config.allowed || config.tokenAddress == address(0)) { return (false, 0); } // Check amount limits if (amount < config.minAmount || amount > config.maxAmount) { return (false, 0); } // Check destination is allowed for this token bool destAllowed = false; for (uint256 i = 0; i < config.allowedDestinations.length; i++) { if (config.allowedDestinations[i] == destinationChainId) { destAllowed = true; break; } } if (!destAllowed) { return (false, 0); } // Calculate fee (base fee + token-specific fee) uint256 baseFeeAmount = (amount * dest.baseFee) / 10000; uint256 tokenFeeAmount = (amount * config.bridgeFeeBps) / 10000; fee = baseFeeAmount + tokenFeeAmount; return (true, fee); } /** * @notice Get route health score (0-10000, higher is better) * @param chainId Destination chain ID * @param token Token address * @return score Health score */ function getRouteHealthScore( uint256 chainId, address token ) external view returns (uint256 score) { RouteHealth memory health = routeHealth[chainId][token]; uint256 total = health.successCount + health.failureCount; if (total == 0) return 5000; // Default 50% if no data score = (health.successCount * 10000) / total; return score; } /** * @notice Get all registered destinations * @return chainIds Array of chain IDs */ function getAllDestinations() external view returns (uint256[] memory) { return destinationChainIds; } /** * @notice Get all registered tokens * @return tokens Array of token addresses */ function getAllTokens() external view returns (address[] memory) { return registeredTokens; } /** * @notice Pause registry */ function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); } /** * @notice Unpause registry */ function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); } }