// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./IRouterClient.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title CCIP Router Implementation * @notice Full Chainlink CCIP Router interface implementation * @dev Implements message sending, fee calculation, and message validation */ contract CCIPRouter is IRouterClient { using SafeERC20 for IERC20; // Fee token (LINK token address) address public immutable feeToken; // Message tracking mapping(bytes32 => bool) public sentMessages; mapping(bytes32 => bool) public receivedMessages; // Chain selectors mapping(uint64 => bool) public supportedChains; mapping(uint64 => address[]) public supportedTokens; // Fee configuration uint256 public baseFee; // Base fee in feeToken units uint256 public dataFeePerByte; // Fee per byte of data address public admin; // Events are inherited from IRouterClient interface modifier onlyAdmin() { require(msg.sender == admin, "CCIPRouter: only admin"); _; } constructor(address _feeToken, uint256 _baseFee, uint256 _dataFeePerByte) { // Allow zero address for native token fees (ETH) // If feeToken is zero, fees are paid in native token (msg.value) feeToken = _feeToken; baseFee = _baseFee; dataFeePerByte = _dataFeePerByte; admin = msg.sender; } /** * @notice Send a message to a destination chain * @param destinationChainSelector The chain selector of the destination chain * @param message The message to send * @return messageId The ID of the sent message * @return fees The fees required for the message */ function ccipSend( uint64 destinationChainSelector, EVM2AnyMessage memory message ) external payable returns (bytes32 messageId, uint256 fees) { require(supportedChains[destinationChainSelector], "CCIPRouter: chain not supported"); require(message.receiver.length > 0, "CCIPRouter: empty receiver"); // Calculate fee fees = getFee(destinationChainSelector, message); // Collect fee if (fees > 0) { if (feeToken == address(0)) { // Native token (ETH) fees require(msg.value >= fees, "CCIPRouter: insufficient native token fee"); } else { // ERC20 token fees IERC20(feeToken).safeTransferFrom(msg.sender, address(this), fees); } } // Generate message ID messageId = keccak256(abi.encodePacked( block.chainid, destinationChainSelector, msg.sender, message.receiver, message.data, block.timestamp, block.number )); require(!sentMessages[messageId], "CCIPRouter: duplicate message"); sentMessages[messageId] = true; emit MessageSent( messageId, destinationChainSelector, msg.sender, message.receiver, message.data, message.tokenAmounts, message.feeToken, message.extraArgs ); return (messageId, fees); } /** * @notice Get the fee for sending a message * @param destinationChainSelector The chain selector of the destination chain * @param message The message to send * @return fee The fee required for the message */ function getFee( uint64 destinationChainSelector, EVM2AnyMessage memory message ) public view returns (uint256 fee) { require(supportedChains[destinationChainSelector], "CCIPRouter: chain not supported"); // Base fee fee = baseFee; // Data fee (per byte) fee += message.data.length * dataFeePerByte; // Token transfer fees for (uint256 i = 0; i < message.tokenAmounts.length; i++) { fee += message.tokenAmounts[i].amount / 1000; // 0.1% of token amount } return fee; } /** * @notice Get supported tokens for a destination chain * @param destinationChainSelector The chain selector of the destination chain * @return tokens The list of supported tokens */ function getSupportedTokens( uint64 destinationChainSelector ) external view returns (address[] memory tokens) { return supportedTokens[destinationChainSelector]; } /** * @notice Add supported chain */ function addSupportedChain(uint64 chainSelector) external onlyAdmin { supportedChains[chainSelector] = true; } /** * @notice Remove supported chain */ function removeSupportedChain(uint64 chainSelector) external onlyAdmin { supportedChains[chainSelector] = false; } /** * @notice Add supported token for a chain */ function addSupportedToken(uint64 chainSelector, address token) external onlyAdmin { require(token != address(0), "CCIPRouter: zero token"); address[] storage tokens = supportedTokens[chainSelector]; for (uint256 i = 0; i < tokens.length; i++) { require(tokens[i] != token, "CCIPRouter: token already supported"); } tokens.push(token); } /** * @notice Update fee configuration */ function updateFees(uint256 _baseFee, uint256 _dataFeePerByte) external onlyAdmin { baseFee = _baseFee; dataFeePerByte = _dataFeePerByte; } /** * @notice Change admin */ function changeAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "CCIPRouter: zero address"); admin = newAdmin; } /** * @notice Withdraw collected fees */ function withdrawFees(uint256 amount) external onlyAdmin { if (feeToken == address(0)) { // Native token (ETH) fees payable(admin).transfer(amount); } else { // ERC20 token fees IERC20(feeToken).safeTransfer(admin, amount); } } /** * @notice Withdraw all native token (ETH) fees */ function withdrawNativeFees() external onlyAdmin { require(feeToken == address(0), "CCIPRouter: not native token"); payable(admin).transfer(address(this).balance); } /** * @notice Receive native token (ETH) */ receive() external payable {} }