// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "../ccip/IRouterClient.sol"; /** * @title CCIPLogger * @notice Receives and logs Chain-138 transactions via Chainlink CCIP * @dev Implements replay protection via batch ID tracking; optional authorized signer for future use */ contract CCIPLogger { IRouterClient public immutable router; address public authorizedSigner; uint64 public expectedSourceChainSelector; address public owner; mapping(bytes32 => bool) public processedBatches; event RemoteBatchLogged( bytes32 indexed messageId, bytes32 indexed batchId, uint64 sourceChainSelector, address sender, bytes32[] txHashes, address[] froms, address[] tos, uint256[] values ); event AuthorizedSignerUpdated(address oldSigner, address newSigner); event SourceChainSelectorUpdated(uint64 oldSelector, uint64 newSelector); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); modifier onlyRouter() { require(msg.sender == address(router), "CCIPLogger: only router"); _; } modifier onlyOwner() { require(msg.sender == owner, "CCIPLogger: only owner"); _; } constructor( address _router, address _authorizedSigner, uint64 _expectedSourceChainSelector ) { require(_router != address(0), "CCIPLogger: zero router"); router = IRouterClient(_router); authorizedSigner = _authorizedSigner; expectedSourceChainSelector = _expectedSourceChainSelector; owner = msg.sender; } /** * @notice Handle CCIP message (called by CCIP Router) * @param message The received CCIP message */ function ccipReceive( IRouterClient.Any2EVMMessage calldata message ) external onlyRouter { require( message.sourceChainSelector == expectedSourceChainSelector, "CCIPLogger: invalid source chain" ); ( bytes32 batchId, bytes32[] memory txHashes, address[] memory froms, address[] memory tos, uint256[] memory values, bytes memory _extra ) = abi.decode( message.data, (bytes32, bytes32[], address[], address[], uint256[], bytes) ); require(!processedBatches[batchId], "CCIPLogger: batch already processed"); processedBatches[batchId] = true; address sender = message.sender.length >= 20 ? address(bytes20(message.sender)) : address(0); if (message.sender.length == 32) { sender = address(bytes20(message.sender)); } emit RemoteBatchLogged( message.messageId, batchId, message.sourceChainSelector, sender, txHashes, froms, tos, values ); } function getRouter() external view returns (address) { return address(router); } function setAuthorizedSigner(address _signer) external onlyOwner { address old = authorizedSigner; authorizedSigner = _signer; emit AuthorizedSignerUpdated(old, _signer); } function setExpectedSourceChainSelector(uint64 _selector) external onlyOwner { uint64 old = expectedSourceChainSelector; expectedSourceChainSelector = _selector; emit SourceChainSelectorUpdated(old, _selector); } function transferOwnership(address newOwner) external onlyOwner { require(newOwner != address(0), "CCIPLogger: zero address"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; } }