// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "../ccip/IRouterClient.sol"; import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/contracts/interfaces/IAny2EVMMessageReceiver.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; /** * @title CCIP Relay Router * @notice Relay router that forwards CCIP messages from off-chain relay to bridge contracts * @dev This contract acts as a relay endpoint on the destination chain */ contract CCIPRelayRouter is AccessControl { bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE"); // Mapping of bridge contracts that can receive messages mapping(address => bool) public authorizedBridges; event MessageRelayed( bytes32 indexed messageId, uint64 indexed sourceChainSelector, address indexed bridge, address recipient, uint256 amount ); event BridgeAuthorized(address indexed bridge); event BridgeRevoked(address indexed bridge); constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } /** * @notice Authorize a bridge contract to receive relayed messages */ function authorizeBridge(address bridge) external onlyRole(DEFAULT_ADMIN_ROLE) { authorizedBridges[bridge] = true; emit BridgeAuthorized(bridge); } /** * @notice Revoke authorization for a bridge contract */ function revokeBridge(address bridge) external onlyRole(DEFAULT_ADMIN_ROLE) { authorizedBridges[bridge] = false; emit BridgeRevoked(bridge); } /** * @notice Grant relayer role to an address */ function grantRelayerRole(address relayer) external onlyRole(DEFAULT_ADMIN_ROLE) { _grantRole(RELAYER_ROLE, relayer); } /** * @notice Revoke relayer role from an address */ function revokeRelayerRole(address relayer) external onlyRole(DEFAULT_ADMIN_ROLE) { _revokeRole(RELAYER_ROLE, relayer); } /** * @notice Relay a CCIP message to a bridge contract * @param bridge The bridge contract address to receive the message * @param message The CCIP message to relay */ function relayMessage( address bridge, IRouterClient.Any2EVMMessage calldata message ) external onlyRole(RELAYER_ROLE) { require(authorizedBridges[bridge], "CCIPRelayRouter: bridge not authorized"); // CWMultiTokenBridgeL2 and official CCIP receivers use Client.Any2EVMMessage // (destTokenAmounts: EVMTokenAmount[]). Legacy CCIPRelayBridge uses IRouterClient // Any2EVMMessage (tokenAmounts with amountType). Try Client format first, then legacy. Client.Any2EVMMessage memory clientMessage = _toClientMessage(message); (bool success, bytes memory returnData) = bridge.call( abi.encodeCall(IAny2EVMMessageReceiver.ccipReceive, (clientMessage)) ); if (!success) { (success, returnData) = bridge.call( abi.encodeWithSignature( "ccipReceive((bytes32,uint64,bytes,bytes,(address,uint256,uint8)[]))", message ) ); } if (!success && returnData.length > 0) { assembly { revert(add(returnData, 32), mload(returnData)) } } require(success, "CCIPRelayRouter: ccipReceive failed"); // If we get here, the call succeeded. Decode common payload shapes without // reverting the full relay transaction on non-WETH bridge payloads. (address recipient, uint256 amount) = _decodeRecipientAndAmount(message.data); emit MessageRelayed( message.messageId, message.sourceChainSelector, bridge, recipient, amount ); } function _decodeRecipientAndAmount(bytes calldata data) internal pure returns (address recipient, uint256 amount) { if (data.length == 64) { return abi.decode(data, (address, uint256)); } if (data.length == 96) { (, recipient, amount) = abi.decode(data, (address, address, uint256)); return (recipient, amount); } if (data.length == 128) { (recipient, amount, , ) = abi.decode(data, (address, uint256, address, uint256)); return (recipient, amount); } return (address(0), 0); } function _toClientMessage( IRouterClient.Any2EVMMessage calldata message ) internal pure returns (Client.Any2EVMMessage memory clientMessage) { Client.EVMTokenAmount[] memory destAmounts = new Client.EVMTokenAmount[](message.tokenAmounts.length); for (uint256 i = 0; i < message.tokenAmounts.length; i++) { destAmounts[i] = Client.EVMTokenAmount({ token: message.tokenAmounts[i].token, amount: message.tokenAmounts[i].amount }); } clientMessage = Client.Any2EVMMessage({ messageId: message.messageId, sourceChainSelector: message.sourceChainSelector, sender: message.sender, data: message.data, destTokenAmounts: destAmounts }); } }