// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./IRouterClient.sol"; // Minimal IERC20 interface for ERC20 tokens (WETH10 and LINK) interface IERC20 { function transferFrom(address from, address to, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } /** * @title CCIP WETH10 Bridge * @notice Cross-chain WETH10 transfer bridge using Chainlink CCIP * @dev Enables users to send WETH10 tokens across chains via CCIP */ contract CCIPWETH10Bridge { IRouterClient public immutable ccipRouter; address public immutable weth10; // WETH10 contract address address public feeToken; // LINK token address address public admin; // Destination chain configurations struct DestinationChain { uint64 chainSelector; address receiverBridge; // Address of corresponding bridge on destination chain bool enabled; } mapping(uint64 => DestinationChain) public destinations; uint64[] public destinationChains; // Track cross-chain transfers for replay protection mapping(bytes32 => bool) public processedTransfers; mapping(address => uint256) public nonces; event CrossChainTransferInitiated( bytes32 indexed messageId, address indexed sender, uint64 indexed destinationChainSelector, address recipient, uint256 amount, uint256 nonce ); event CrossChainTransferCompleted( bytes32 indexed messageId, uint64 indexed sourceChainSelector, address indexed recipient, uint256 amount ); event DestinationAdded(uint64 chainSelector, address receiverBridge); event DestinationRemoved(uint64 chainSelector); event DestinationUpdated(uint64 chainSelector, address receiverBridge); modifier onlyAdmin() { require(msg.sender == admin, "CCIPWETH10Bridge: only admin"); _; } modifier onlyRouter() { require(msg.sender == address(ccipRouter), "CCIPWETH10Bridge: only router"); _; } constructor(address _ccipRouter, address _weth10, address _feeToken) { require(_ccipRouter != address(0), "CCIPWETH10Bridge: zero router"); require(_weth10 != address(0), "CCIPWETH10Bridge: zero WETH10"); require(_feeToken != address(0), "CCIPWETH10Bridge: zero fee token"); ccipRouter = IRouterClient(_ccipRouter); weth10 = _weth10; feeToken = _feeToken; admin = msg.sender; } /** * @notice Send WETH10 tokens to another chain via CCIP * @param destinationChainSelector The chain selector of the destination chain * @param recipient The recipient address on the destination chain * @param amount The amount of WETH10 to send * @return messageId The CCIP message ID */ function sendCrossChain( uint64 destinationChainSelector, address recipient, uint256 amount ) external returns (bytes32 messageId) { require(amount > 0, "CCIPWETH10Bridge: invalid amount"); require(recipient != address(0), "CCIPWETH10Bridge: zero recipient"); DestinationChain memory dest = destinations[destinationChainSelector]; require(dest.enabled, "CCIPWETH10Bridge: destination not enabled"); // Transfer WETH10 from user require(IERC20(weth10).transferFrom(msg.sender, address(this), amount), "CCIPWETH10Bridge: transfer failed"); // Increment nonce for replay protection nonces[msg.sender]++; uint256 currentNonce = nonces[msg.sender]; // Encode transfer data (recipient, amount, sender, nonce) bytes memory data = abi.encode( recipient, amount, msg.sender, currentNonce ); // Prepare CCIP message with WETH10 tokens IRouterClient.EVM2AnyMessage memory message = IRouterClient.EVM2AnyMessage({ receiver: abi.encode(dest.receiverBridge), data: data, tokenAmounts: new IRouterClient.TokenAmount[](1), feeToken: feeToken, extraArgs: "" }); // Set token amount (WETH10) message.tokenAmounts[0] = IRouterClient.TokenAmount({ token: weth10, amount: amount, amountType: IRouterClient.TokenAmountType.Fiat }); // Calculate fee uint256 fee = ccipRouter.getFee(destinationChainSelector, message); // Approve and pay fee if (fee > 0) { require(IERC20(feeToken).transferFrom(msg.sender, address(this), fee), "CCIPWETH10Bridge: fee transfer failed"); require(IERC20(feeToken).approve(address(ccipRouter), fee), "CCIPWETH10Bridge: fee approval failed"); } // Send via CCIP (messageId, ) = ccipRouter.ccipSend(destinationChainSelector, message); emit CrossChainTransferInitiated( messageId, msg.sender, destinationChainSelector, recipient, amount, currentNonce ); return messageId; } /** * @notice Receive WETH10 tokens from another chain via CCIP * @param message The CCIP message */ function ccipReceive( IRouterClient.Any2EVMMessage calldata message ) external onlyRouter { // Replay protection: check if message already processed require(!processedTransfers[message.messageId], "CCIPWETH10Bridge: transfer already processed"); // Mark as processed processedTransfers[message.messageId] = true; // Validate token amounts require(message.tokenAmounts.length > 0, "CCIPWETH10Bridge: no tokens"); require(message.tokenAmounts[0].token == weth10, "CCIPWETH10Bridge: invalid token"); uint256 amount = message.tokenAmounts[0].amount; require(amount > 0, "CCIPWETH10Bridge: invalid amount"); // Decode transfer data (recipient, amount, sender, nonce) (address recipient, , , ) = abi.decode( message.data, (address, uint256, address, uint256) ); require(recipient != address(0), "CCIPWETH10Bridge: zero recipient"); // Transfer WETH10 to recipient require(IERC20(weth10).transfer(recipient, amount), "CCIPWETH10Bridge: transfer failed"); emit CrossChainTransferCompleted( message.messageId, message.sourceChainSelector, recipient, amount ); } /** * @notice Calculate fee for cross-chain transfer * @param destinationChainSelector The chain selector of the destination chain * @param amount The amount of WETH10 to send * @return fee The fee required for the transfer */ function calculateFee( uint64 destinationChainSelector, uint256 amount ) external view returns (uint256 fee) { DestinationChain memory dest = destinations[destinationChainSelector]; require(dest.enabled, "CCIPWETH10Bridge: destination not enabled"); bytes memory data = abi.encode(address(0), amount, address(0), 0); IRouterClient.EVM2AnyMessage memory message = IRouterClient.EVM2AnyMessage({ receiver: abi.encode(dest.receiverBridge), data: data, tokenAmounts: new IRouterClient.TokenAmount[](1), feeToken: feeToken, extraArgs: "" }); message.tokenAmounts[0] = IRouterClient.TokenAmount({ token: weth10, amount: amount, amountType: IRouterClient.TokenAmountType.Fiat }); return ccipRouter.getFee(destinationChainSelector, message); } /** * @notice Add destination chain */ function addDestination( uint64 chainSelector, address receiverBridge ) external onlyAdmin { require(receiverBridge != address(0), "CCIPWETH10Bridge: zero address"); require(!destinations[chainSelector].enabled, "CCIPWETH10Bridge: destination already exists"); destinations[chainSelector] = DestinationChain({ chainSelector: chainSelector, receiverBridge: receiverBridge, enabled: true }); destinationChains.push(chainSelector); emit DestinationAdded(chainSelector, receiverBridge); } /** * @notice Remove destination chain */ function removeDestination(uint64 chainSelector) external onlyAdmin { require(destinations[chainSelector].enabled, "CCIPWETH10Bridge: destination not found"); destinations[chainSelector].enabled = false; // Remove from array for (uint256 i = 0; i < destinationChains.length; i++) { if (destinationChains[i] == chainSelector) { destinationChains[i] = destinationChains[destinationChains.length - 1]; destinationChains.pop(); break; } } emit DestinationRemoved(chainSelector); } /** * @notice Update destination receiver bridge */ function updateDestination( uint64 chainSelector, address receiverBridge ) external onlyAdmin { require(destinations[chainSelector].enabled, "CCIPWETH10Bridge: destination not found"); require(receiverBridge != address(0), "CCIPWETH10Bridge: zero address"); destinations[chainSelector].receiverBridge = receiverBridge; emit DestinationUpdated(chainSelector, receiverBridge); } /** * @notice Update fee token */ function updateFeeToken(address newFeeToken) external onlyAdmin { require(newFeeToken != address(0), "CCIPWETH10Bridge: zero address"); feeToken = newFeeToken; } /** * @notice Change admin */ function changeAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "CCIPWETH10Bridge: zero address"); admin = newAdmin; } /** * @notice Get destination chains */ function getDestinationChains() external view returns (uint64[] memory) { return destinationChains; } /** * @notice Get user nonce */ function getUserNonce(address user) external view returns (uint256) { return nonces[user]; } }