// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../ccip/IRouterClient.sol"; interface IMintableERC20 { function mint(address to, uint256 amount) external; function burnFrom(address from, uint256 amount) external; function balanceOf(address account) external view returns (uint256); } /** * @title TwoWayTokenBridgeL2 * @notice L2/secondary chain side: mints mirrored tokens on inbound and burns on outbound */ contract TwoWayTokenBridgeL2 { IRouterClient public immutable ccipRouter; address public immutable mirroredToken; address public feeToken; // LINK address public admin; struct DestinationConfig { uint64 chainSelector; address l1Bridge; bool enabled; } mapping(uint64 => DestinationConfig) public destinations; uint64[] public destinationChains; mapping(bytes32 => bool) public processed; event Minted(address indexed recipient, uint256 amount); event Burned(address indexed user, uint256 amount); event CcipSend(bytes32 indexed messageId, uint64 destChain, address recipient, uint256 amount); event DestinationAdded(uint64 chainSelector, address l1Bridge); event DestinationUpdated(uint64 chainSelector, address l1Bridge); event DestinationRemoved(uint64 chainSelector); modifier onlyAdmin() { require(msg.sender == admin, "only admin"); _; } modifier onlyRouter() { require(msg.sender == address(ccipRouter), "only router"); _; } constructor(address _router, address _token, address _feeToken) { require(_router != address(0) && _token != address(0) && _feeToken != address(0), "zero addr"); ccipRouter = IRouterClient(_router); mirroredToken = _token; feeToken = _feeToken; admin = msg.sender; } function addDestination(uint64 chainSelector, address l1Bridge) external onlyAdmin { require(l1Bridge != address(0), "zero l1"); require(!destinations[chainSelector].enabled, "exists"); destinations[chainSelector] = DestinationConfig(chainSelector, l1Bridge, true); destinationChains.push(chainSelector); emit DestinationAdded(chainSelector, l1Bridge); } function updateDestination(uint64 chainSelector, address l1Bridge) external onlyAdmin { require(destinations[chainSelector].enabled, "missing"); require(l1Bridge != address(0), "zero l1"); destinations[chainSelector].l1Bridge = l1Bridge; emit DestinationUpdated(chainSelector, l1Bridge); } function removeDestination(uint64 chainSelector) external onlyAdmin { require(destinations[chainSelector].enabled, "missing"); destinations[chainSelector].enabled = false; for (uint256 i = 0; i < destinationChains.length; i++) { if (destinationChains[i] == chainSelector) { destinationChains[i] = destinationChains[destinationChains.length - 1]; destinationChains.pop(); break; } } emit DestinationRemoved(chainSelector); } function updateFeeToken(address newFee) external onlyAdmin { require(newFee != address(0), "zero"); feeToken = newFee; } function changeAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "zero"); admin = newAdmin; } function getDestinationChains() external view returns (uint64[] memory) { return destinationChains; } // Inbound from L1: mint mirrored tokens to recipient function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyRouter { require(!processed[message.messageId], "replayed"); processed[message.messageId] = true; (address recipient, uint256 amount) = abi.decode(message.data, (address, uint256)); require(recipient != address(0) && amount > 0, "bad msg"); IMintableERC20(mirroredToken).mint(recipient, amount); emit Minted(recipient, amount); } // Outbound to L1: burn mirrored tokens and signal release on L1 function burnAndSend(uint64 destSelector, address recipient, uint256 amount) external returns (bytes32 messageId) { require(amount > 0 && recipient != address(0), "bad args"); DestinationConfig memory dest = destinations[destSelector]; require(dest.enabled, "dest disabled"); IMintableERC20(mirroredToken).burnFrom(msg.sender, amount); emit Burned(msg.sender, amount); bytes memory data = abi.encode(recipient, amount); IRouterClient.EVM2AnyMessage memory m = IRouterClient.EVM2AnyMessage({ receiver: abi.encode(dest.l1Bridge), data: data, tokenAmounts: new IRouterClient.TokenAmount[](0), feeToken: feeToken, extraArgs: "" }); uint256 fee = ccipRouter.getFee(destSelector, m); if (fee > 0) { require(IERC20(feeToken).approve(address(ccipRouter), fee), "fee approve"); } (messageId, ) = ccipRouter.ccipSend(destSelector, m); emit CcipSend(messageId, destSelector, recipient, amount); return messageId; } }