// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../ccip/IRouterClient.sol"; interface ICWMintBurnToken is IERC20 { function mint(address to, uint256 amount) external; function burnFrom(address from, uint256 amount) external; } /** * @title CWMultiTokenBridgeL2 * @notice Destination/public-chain bridge for minting and burning cW* assets without prefunding. * @dev Supports multiple canonical -> mirrored token pairs behind one bridge address. */ contract CWMultiTokenBridgeL2 { using SafeERC20 for IERC20; IRouterClient public sendRouter; address public receiveRouter; address public feeToken; address public admin; struct DestinationConfig { address receiverBridge; bool enabled; } mapping(uint64 => DestinationConfig) public destinations; mapping(address => address) public canonicalToMirrored; mapping(address => address) public mirroredToCanonical; mapping(bytes32 => bool) public processed; mapping(address => bool) public tokenPairFrozen; mapping(uint64 => bool) public destinationFrozen; mapping(address => bool) public paused; mapping(address => uint256) public mintedTotal; mapping(address => uint256) public burnedTotal; event TokenPairConfigured(address indexed canonicalToken, address indexed mirroredToken); event DestinationConfigured(uint64 indexed chainSelector, address receiverBridge, bool enabled); event Minted(address indexed canonicalToken, address indexed mirroredToken, address indexed recipient, uint256 amount); event Burned(address indexed canonicalToken, address indexed mirroredToken, address indexed user, uint256 amount); event MessageSent( bytes32 indexed messageId, address indexed canonicalToken, address indexed mirroredToken, uint64 destinationChainSelector, address recipient, uint256 amount ); event TokenPairFrozen(address indexed canonicalToken, address indexed mirroredToken); event DestinationFrozen(uint64 indexed chainSelector, address receiverBridge); event MirroredTokenPauseUpdated(address indexed mirroredToken, bool paused); event SupplyAccountingUpdated( address indexed canonicalToken, address indexed mirroredToken, uint256 mintedTotalAmount, uint256 burnedTotalAmount, uint256 liveSupply ); event SendRouterUpdated(address indexed newRouter); event ReceiveRouterUpdated(address indexed newRouter); event FeeTokenUpdated(address indexed newFeeToken); event AdminChanged(address indexed newAdmin); modifier onlyAdmin() { require(msg.sender == admin, "CWMultiTokenBridgeL2: only admin"); _; } modifier onlyReceiveRouter() { require(msg.sender == receiveRouter, "CWMultiTokenBridgeL2: only receive router"); _; } constructor(address _sendRouter, address _receiveRouter, address _feeToken) { require(_sendRouter != address(0), "CWMultiTokenBridgeL2: zero send router"); require(_receiveRouter != address(0), "CWMultiTokenBridgeL2: zero receive router"); sendRouter = IRouterClient(_sendRouter); receiveRouter = _receiveRouter; feeToken = _feeToken; admin = msg.sender; } function configureTokenPair(address canonicalToken, address mirroredToken) external onlyAdmin { require(canonicalToken != address(0), "CWMultiTokenBridgeL2: zero canonical"); require(mirroredToken != address(0), "CWMultiTokenBridgeL2: zero mirrored"); require(!tokenPairFrozen[canonicalToken], "CWMultiTokenBridgeL2: token pair frozen"); address previousCanonical = mirroredToCanonical[mirroredToken]; if (previousCanonical != address(0) && previousCanonical != canonicalToken) { require(!tokenPairFrozen[previousCanonical], "CWMultiTokenBridgeL2: token pair frozen"); delete canonicalToMirrored[previousCanonical]; } canonicalToMirrored[canonicalToken] = mirroredToken; mirroredToCanonical[mirroredToken] = canonicalToken; emit TokenPairConfigured(canonicalToken, mirroredToken); } function configureDestination(uint64 chainSelector, address receiverBridge, bool enabled) external onlyAdmin { require(receiverBridge != address(0), "CWMultiTokenBridgeL2: zero bridge"); require(!destinationFrozen[chainSelector], "CWMultiTokenBridgeL2: destination frozen"); destinations[chainSelector] = DestinationConfig({ receiverBridge: receiverBridge, enabled: enabled }); emit DestinationConfigured(chainSelector, receiverBridge, enabled); } function freezeTokenPair(address canonicalToken) external onlyAdmin { address mirroredToken = canonicalToMirrored[canonicalToken]; require(mirroredToken != address(0), "CWMultiTokenBridgeL2: token not configured"); tokenPairFrozen[canonicalToken] = true; emit TokenPairFrozen(canonicalToken, mirroredToken); } function freezeDestination(uint64 chainSelector) external onlyAdmin { address receiverBridge = destinations[chainSelector].receiverBridge; require(receiverBridge != address(0), "CWMultiTokenBridgeL2: destination not configured"); destinationFrozen[chainSelector] = true; emit DestinationFrozen(chainSelector, receiverBridge); } function setTokenPaused(address mirroredToken, bool isPaused) external onlyAdmin { require(mirroredToCanonical[mirroredToken] != address(0), "CWMultiTokenBridgeL2: token not configured"); paused[mirroredToken] = isPaused; emit MirroredTokenPauseUpdated(mirroredToken, isPaused); } function setSendRouter(address newRouter) external onlyAdmin { require(newRouter != address(0), "CWMultiTokenBridgeL2: zero router"); sendRouter = IRouterClient(newRouter); emit SendRouterUpdated(newRouter); } function setReceiveRouter(address newRouter) external onlyAdmin { require(newRouter != address(0), "CWMultiTokenBridgeL2: zero router"); receiveRouter = newRouter; emit ReceiveRouterUpdated(newRouter); } function setFeeToken(address newFeeToken) external onlyAdmin { feeToken = newFeeToken; emit FeeTokenUpdated(newFeeToken); } function changeAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "CWMultiTokenBridgeL2: zero admin"); admin = newAdmin; emit AdminChanged(newAdmin); } function calculateFee( address mirroredToken, uint64 destinationChainSelector, address recipient, uint256 amount ) external view returns (uint256) { address canonicalToken = mirroredToCanonical[mirroredToken]; require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured"); DestinationConfig memory dest = destinations[destinationChainSelector]; require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled"); return sendRouter.getFee(destinationChainSelector, _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount)); } function circulatingSupply(address mirroredToken) external view returns (uint256) { uint256 totalMinted = mintedTotal[mirroredToken]; uint256 totalBurned = burnedTotal[mirroredToken]; return totalMinted > totalBurned ? totalMinted - totalBurned : 0; } function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyReceiveRouter { require(!processed[message.messageId], "CWMultiTokenBridgeL2: replayed"); (address canonicalToken, address recipient, uint256 amount) = abi.decode(message.data, (address, address, uint256)); require(recipient != address(0), "CWMultiTokenBridgeL2: zero recipient"); require(amount > 0, "CWMultiTokenBridgeL2: zero amount"); address mirroredToken = canonicalToMirrored[canonicalToken]; require(mirroredToken != address(0), "CWMultiTokenBridgeL2: token not configured"); require(!paused[mirroredToken], "CWMultiTokenBridgeL2: token paused"); DestinationConfig memory peer = destinations[message.sourceChainSelector]; require(peer.enabled, "CWMultiTokenBridgeL2: peer disabled"); require(_decodeSender(message.sender) == peer.receiverBridge, "CWMultiTokenBridgeL2: peer mismatch"); processed[message.messageId] = true; ICWMintBurnToken(mirroredToken).mint(recipient, amount); mintedTotal[mirroredToken] += amount; emit Minted(canonicalToken, mirroredToken, recipient, amount); emit SupplyAccountingUpdated( canonicalToken, mirroredToken, mintedTotal[mirroredToken], burnedTotal[mirroredToken], IERC20(mirroredToken).totalSupply() ); } function burnAndSend( address mirroredToken, uint64 destinationChainSelector, address recipient, uint256 amount ) external payable returns (bytes32 messageId) { require(recipient != address(0), "CWMultiTokenBridgeL2: zero recipient"); require(amount > 0, "CWMultiTokenBridgeL2: zero amount"); address canonicalToken = mirroredToCanonical[mirroredToken]; require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured"); require(!paused[mirroredToken], "CWMultiTokenBridgeL2: token paused"); DestinationConfig memory dest = destinations[destinationChainSelector]; require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled"); ICWMintBurnToken(mirroredToken).burnFrom(msg.sender, amount); burnedTotal[mirroredToken] += amount; emit Burned(canonicalToken, mirroredToken, msg.sender, amount); emit SupplyAccountingUpdated( canonicalToken, mirroredToken, mintedTotal[mirroredToken], burnedTotal[mirroredToken], IERC20(mirroredToken).totalSupply() ); IRouterClient.EVM2AnyMessage memory message = _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount); uint256 fee = sendRouter.getFee(destinationChainSelector, message); _collectAndApproveFee(fee); (messageId, ) = feeToken == address(0) ? sendRouter.ccipSend{value: fee}(destinationChainSelector, message) : sendRouter.ccipSend(destinationChainSelector, message); emit MessageSent(messageId, canonicalToken, mirroredToken, destinationChainSelector, recipient, amount); _refundNativeExcess(fee); return messageId; } function withdrawToken(address token, address to, uint256 amount) external onlyAdmin { IERC20(token).safeTransfer(to, amount); } function withdrawNative(address payable to, uint256 amount) external onlyAdmin { (bool ok, ) = to.call{value: amount}(""); require(ok, "CWMultiTokenBridgeL2: native transfer failed"); } receive() external payable {} function _buildMessage( address receiverBridge, address canonicalToken, address recipient, uint256 amount ) internal view returns (IRouterClient.EVM2AnyMessage memory message) { message = IRouterClient.EVM2AnyMessage({ receiver: abi.encode(receiverBridge), data: abi.encode(canonicalToken, recipient, amount), tokenAmounts: new IRouterClient.TokenAmount[](0), feeToken: feeToken, extraArgs: "" }); } function _collectAndApproveFee(uint256 fee) internal { if (fee == 0) { return; } if (feeToken == address(0)) { require(msg.value >= fee, "CWMultiTokenBridgeL2: insufficient native fee"); return; } IERC20 feeErc20 = IERC20(feeToken); feeErc20.safeTransferFrom(msg.sender, address(this), fee); feeErc20.forceApprove(address(sendRouter), fee); } function _refundNativeExcess(uint256 feeUsed) internal { if (feeToken != address(0) || msg.value <= feeUsed) { return; } uint256 refund = msg.value - feeUsed; (bool ok, ) = payable(msg.sender).call{value: refund}(""); require(ok, "CWMultiTokenBridgeL2: refund failed"); } function _decodeSender(bytes memory senderData) internal pure returns (address senderAddress) { if (senderData.length == 0) { return address(0); } senderAddress = abi.decode(senderData, (address)); } }