// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IRouterClient} from "../ccip/IRouterClient.sol"; /** * @title CrossChainFlashRepayReceiver * @notice **Destination-chain** CCIP receiver: forwards received ERC-20s to a recipient encoded in `message.data`. * @dev Intended pairing with `CrossChainFlashBorrower` / bridge senders. `msg.sender` must be the CCIP router. * `data` encoding: `abi.encode(address recipient, bytes32 obligationId)` where `obligationId` is opaque (indexing / ops). * **Repaying the source-chain flash vault** requires a **separate** flow (second tx / bridge back); this contract does not pull debt on source. */ contract CrossChainFlashRepayReceiver { using SafeERC20 for IERC20; IRouterClient public immutable ccipRouter; event CrossChainDelivered( bytes32 indexed messageId, uint64 indexed sourceChainSelector, address indexed recipient, address token, uint256 amount, bytes32 obligationId ); error OnlyRouter(); error NoTokens(); error BadData(); constructor(address ccipRouter_) { require(ccipRouter_ != address(0), "CrossChainFlashRepayReceiver: zero router"); ccipRouter = IRouterClient(ccipRouter_); } modifier onlyRouter() { if (msg.sender != address(ccipRouter)) revert OnlyRouter(); _; } function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyRouter { _deliver(message); } function _deliver(IRouterClient.Any2EVMMessage calldata m) private { if (m.tokenAmounts.length == 0) revert NoTokens(); IRouterClient.TokenAmount calldata ta = m.tokenAmounts[0]; if (ta.token == address(0) || ta.amount == 0) revert NoTokens(); (address recipient, bytes32 obligationId) = abi.decode(m.data, (address, bytes32)); if (recipient == address(0)) revert BadData(); IERC20(ta.token).safeTransfer(recipient, ta.amount); emit CrossChainDelivered(m.messageId, m.sourceChainSelector, recipient, ta.token, ta.amount, obligationId); } }