// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../registry/UniversalAssetRegistry.sol"; import "../ccip/IRouterClient.sol"; /** * @title UniversalCCIPBridge * @notice Main bridge contract supporting all asset types via CCIP * @dev Extends CCIP infrastructure with dynamic asset routing and PMM integration */ contract UniversalCCIPBridge is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { using SafeERC20 for IERC20; bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); struct BridgeOperation { address token; uint256 amount; uint64 destinationChain; address recipient; bytes32 assetType; bool usePMM; bool useVault; bytes complianceProof; bytes vaultInstructions; } struct Destination { address receiverBridge; bool enabled; uint256 addedAt; } // Core dependencies UniversalAssetRegistry public assetRegistry; IRouterClient public ccipRouter; address public liquidityManager; address public vaultFactory; // State mapping(address => mapping(uint64 => Destination)) public destinations; mapping(address => address) public userVaults; mapping(bytes32 => bool) public processedMessages; mapping(address => uint256) public nonces; // Events event BridgeExecuted( bytes32 indexed messageId, address indexed token, address indexed sender, uint256 amount, uint64 destinationChain, address recipient, bool usedPMM ); event DestinationAdded( address indexed token, uint64 indexed chainSelector, address receiverBridge ); event DestinationRemoved( address indexed token, uint64 indexed chainSelector ); event MessageReceived( bytes32 indexed messageId, uint64 indexed sourceChainSelector, address sender, address token, uint256 amount ); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize( address _assetRegistry, address _ccipRouter, address admin ) external initializer { __AccessControl_init(); __ReentrancyGuard_init(); __UUPSUpgradeable_init(); require(_assetRegistry != address(0), "Zero registry"); assetRegistry = UniversalAssetRegistry(_assetRegistry); if (_ccipRouter != address(0)) { ccipRouter = IRouterClient(_ccipRouter); } // If _ccipRouter is zero, set via setCCIPRouter() after deployment (enables same initData for deterministic proxy address) _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(BRIDGE_OPERATOR_ROLE, admin); _grantRole(UPGRADER_ROLE, admin); } function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} /** * @notice Set CCIP router (for deterministic deployment: initialize with router=0, then set per chain) */ function setCCIPRouter(address _ccipRouter) external onlyRole(DEFAULT_ADMIN_ROLE) { require(_ccipRouter != address(0), "Zero router"); ccipRouter = IRouterClient(_ccipRouter); } /** * @notice Main bridge function with asset type routing */ function bridge( BridgeOperation calldata op ) external payable nonReentrant returns (bytes32 messageId) { // Validate asset is registered and active UniversalAssetRegistry.UniversalAsset memory asset = assetRegistry.getAsset(op.token); require(asset.isActive, "Asset not active"); require(asset.tokenAddress != address(0), "Asset not registered"); // Verify destination is enabled Destination memory dest = destinations[op.token][op.destinationChain]; require(dest.enabled, "Destination not enabled"); require(dest.receiverBridge != address(0), "Invalid receiver"); // Validate amounts require(op.amount > 0, "Invalid amount"); require(op.amount >= asset.minBridgeAmount, "Below minimum"); require(op.amount <= asset.maxBridgeAmount, "Above maximum"); // Transfer tokens from user IERC20(op.token).safeTransferFrom(msg.sender, address(this), op.amount); // Execute bridge with optional PMM if (op.usePMM && liquidityManager != address(0)) { _executeBridgeWithPMM(op); } // Execute bridge with optional vault if (op.useVault && vaultFactory != address(0)) { _executeBridgeWithVault(op); } // Send CCIP message messageId = _sendCCIPMessage(op, dest); // Increment nonce nonces[msg.sender]++; emit BridgeExecuted( messageId, op.token, msg.sender, op.amount, op.destinationChain, op.recipient, op.usePMM ); return messageId; } /** * @notice Execute bridge with PMM liquidity */ function _executeBridgeWithPMM(BridgeOperation calldata op) internal { if (liquidityManager == address(0)) return; // Call liquidity manager to provide liquidity (bool success, ) = liquidityManager.call( abi.encodeWithSignature( "provideLiquidity(address,uint256,bytes)", op.token, op.amount, "" ) ); // PMM is optional, don't revert if it fails if (!success) { // Log or handle PMM failure gracefully } } /** * @notice Execute bridge with vault */ function _executeBridgeWithVault(BridgeOperation calldata op) internal { if (vaultFactory == address(0)) return; // Get or create vault for user address vault = userVaults[msg.sender]; if (vault == address(0)) { // Call vault factory to create vault (bool success, bytes memory data) = vaultFactory.call( abi.encodeWithSignature("createVault(address)", msg.sender) ); if (success) { vault = abi.decode(data, (address)); userVaults[msg.sender] = vault; } } // If vault exists, record operation if (vault != address(0)) { // Call vault to record bridge operation (bool ok, ) = vault.call( abi.encodeWithSignature( "recordBridgeOperation(bytes32,address,uint256,uint64)", bytes32(0), // messageId will be set after CCIP send op.token, op.amount, op.destinationChain ) ); if (!ok) { // Vault recording is optional; ignore failure } } } /** * @notice Send CCIP message */ function _sendCCIPMessage( BridgeOperation calldata op, Destination memory dest ) internal returns (bytes32 messageId) { // Encode message data bytes memory data = abi.encode( op.recipient, op.amount, msg.sender, nonces[msg.sender] ); // Prepare CCIP message IRouterClient.EVM2AnyMessage memory message = IRouterClient.EVM2AnyMessage({ receiver: abi.encode(dest.receiverBridge), data: data, tokenAmounts: new IRouterClient.TokenAmount[](1), feeToken: address(0), // Pay in native extraArgs: "" }); // Set token amount message.tokenAmounts[0] = IRouterClient.TokenAmount({ token: op.token, amount: op.amount, amountType: IRouterClient.TokenAmountType.Fiat }); // Calculate fee uint256 fee = ccipRouter.getFee(op.destinationChain, message); require(address(this).balance >= fee, "Insufficient fee"); // Send via CCIP (messageId, ) = ccipRouter.ccipSend{value: fee}(op.destinationChain, message); return messageId; } /** * @notice Add destination for token */ function addDestination( address token, uint64 chainSelector, address receiverBridge ) external onlyRole(BRIDGE_OPERATOR_ROLE) { require(token != address(0), "Zero token"); require(receiverBridge != address(0), "Zero receiver"); destinations[token][chainSelector] = Destination({ receiverBridge: receiverBridge, enabled: true, addedAt: block.timestamp }); emit DestinationAdded(token, chainSelector, receiverBridge); } /** * @notice Remove destination */ function removeDestination( address token, uint64 chainSelector ) external onlyRole(BRIDGE_OPERATOR_ROLE) { destinations[token][chainSelector].enabled = false; emit DestinationRemoved(token, chainSelector); } /** * @notice Set liquidity manager */ function setLiquidityManager(address _liquidityManager) external onlyRole(DEFAULT_ADMIN_ROLE) { liquidityManager = _liquidityManager; } /** * @notice Set vault factory */ function setVaultFactory(address _vaultFactory) external onlyRole(DEFAULT_ADMIN_ROLE) { vaultFactory = _vaultFactory; } /** * @notice Receive native tokens */ receive() external payable {} /** * @notice Withdraw native tokens */ function withdraw() external onlyRole(DEFAULT_ADMIN_ROLE) { payable(msg.sender).transfer(address(this).balance); } // View functions function getDestination(address token, uint64 chainSelector) external view returns (Destination memory) { return destinations[token][chainSelector]; } function getUserVault(address user) external view returns (address) { return userVaults[user]; } function getUserNonce(address user) external view returns (uint256) { return nonces[user]; } }