// 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"; import "./integration/ICWReserveVerifier.sol"; /** * @title CWMultiTokenBridgeL1 * @notice Chain 138 side escrow bridge for non-prefunded c* -> cW* routing. * @dev Locks canonical c* tokens on send and releases them on return messages. */ contract CWMultiTokenBridgeL1 { using SafeERC20 for IERC20; IRouterClient public sendRouter; address public receiveRouter; address public feeToken; address public admin; address public reserveVerifier; struct DestinationConfig { address receiverBridge; bool enabled; } mapping(address => mapping(uint64 => DestinationConfig)) public destinations; mapping(bytes32 => bool) public processed; mapping(address => bool) public supportedCanonicalToken; mapping(address => bool) public paused; mapping(address => uint256) public lockedBalance; mapping(address => uint256) public totalOutstanding; mapping(address => mapping(uint64 => uint256)) public outstandingMinted; mapping(address => mapping(uint64 => uint256)) public maxOutstanding; event Locked(address indexed canonicalToken, address indexed user, uint256 amount); event Released(address indexed canonicalToken, address indexed recipient, uint256 amount); event MessageSent( bytes32 indexed messageId, address indexed canonicalToken, uint64 indexed destinationChainSelector, address recipient, uint256 amount ); event DestinationConfigured(address indexed canonicalToken, uint64 indexed chainSelector, address receiverBridge, bool enabled); event SupportedCanonicalTokenConfigured(address indexed canonicalToken, bool enabled); event TokenPauseUpdated(address indexed canonicalToken, bool paused); event MaxOutstandingUpdated(address indexed canonicalToken, uint64 indexed chainSelector, uint256 maxOutstandingAmount); event OutstandingUpdated( address indexed canonicalToken, uint64 indexed chainSelector, uint256 chainOutstanding, uint256 totalOutstandingAmount, uint256 lockedBalanceAmount ); event SendRouterUpdated(address indexed newRouter); event ReceiveRouterUpdated(address indexed newRouter); event FeeTokenUpdated(address indexed newFeeToken); event AdminChanged(address indexed newAdmin); event ReserveVerifierUpdated(address indexed newVerifier); modifier onlyAdmin() { require(msg.sender == admin, "CWMultiTokenBridgeL1: only admin"); _; } modifier onlyReceiveRouter() { require(msg.sender == receiveRouter, "CWMultiTokenBridgeL1: only receive router"); _; } constructor(address _sendRouter, address _receiveRouter, address _feeToken) { require(_sendRouter != address(0), "CWMultiTokenBridgeL1: zero send router"); require(_receiveRouter != address(0), "CWMultiTokenBridgeL1: zero receive router"); sendRouter = IRouterClient(_sendRouter); receiveRouter = _receiveRouter; feeToken = _feeToken; admin = msg.sender; } function configureDestination( address canonicalToken, uint64 chainSelector, address receiverBridge, bool enabled ) external onlyAdmin { require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token"); require(receiverBridge != address(0), "CWMultiTokenBridgeL1: zero bridge"); destinations[canonicalToken][chainSelector] = DestinationConfig({ receiverBridge: receiverBridge, enabled: enabled }); emit DestinationConfigured(canonicalToken, chainSelector, receiverBridge, enabled); } function configureSupportedCanonicalToken(address canonicalToken, bool enabled) external onlyAdmin { require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token"); if (!enabled) { require(lockedBalance[canonicalToken] == 0, "CWMultiTokenBridgeL1: token still locked"); require(totalOutstanding[canonicalToken] == 0, "CWMultiTokenBridgeL1: token still outstanding"); } supportedCanonicalToken[canonicalToken] = enabled; emit SupportedCanonicalTokenConfigured(canonicalToken, enabled); } function setTokenPaused(address canonicalToken, bool isPaused) external onlyAdmin { require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token"); paused[canonicalToken] = isPaused; emit TokenPauseUpdated(canonicalToken, isPaused); } function setMaxOutstanding(address canonicalToken, uint64 chainSelector, uint256 amount) external onlyAdmin { require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token"); maxOutstanding[canonicalToken][chainSelector] = amount; emit MaxOutstandingUpdated(canonicalToken, chainSelector, amount); } function setSendRouter(address newRouter) external onlyAdmin { require(newRouter != address(0), "CWMultiTokenBridgeL1: zero router"); sendRouter = IRouterClient(newRouter); emit SendRouterUpdated(newRouter); } function setReceiveRouter(address newRouter) external onlyAdmin { require(newRouter != address(0), "CWMultiTokenBridgeL1: 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), "CWMultiTokenBridgeL1: zero admin"); admin = newAdmin; emit AdminChanged(newAdmin); } function setReserveVerifier(address newVerifier) external onlyAdmin { reserveVerifier = newVerifier; emit ReserveVerifierUpdated(newVerifier); } function calculateFee( address canonicalToken, uint64 destinationChainSelector, address recipient, uint256 amount ) external view returns (uint256) { DestinationConfig memory dest = destinations[canonicalToken][destinationChainSelector]; require(dest.enabled, "CWMultiTokenBridgeL1: destination disabled"); return sendRouter.getFee(destinationChainSelector, _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount)); } function availableToMint( address canonicalToken, uint64 destinationChainSelector ) public view returns (uint256) { return _availableToMint( canonicalToken, destinationChainSelector, lockedBalance[canonicalToken], totalOutstanding[canonicalToken], outstandingMinted[canonicalToken][destinationChainSelector] ); } function lockAndSend( address canonicalToken, uint64 destinationChainSelector, address recipient, uint256 amount ) external payable returns (bytes32 messageId) { require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token"); require(recipient != address(0), "CWMultiTokenBridgeL1: zero recipient"); require(amount > 0, "CWMultiTokenBridgeL1: zero amount"); require(supportedCanonicalToken[canonicalToken], "CWMultiTokenBridgeL1: unsupported token"); require(!paused[canonicalToken], "CWMultiTokenBridgeL1: token paused"); DestinationConfig memory dest = destinations[canonicalToken][destinationChainSelector]; require(dest.enabled, "CWMultiTokenBridgeL1: destination disabled"); IERC20(canonicalToken).safeTransferFrom(msg.sender, address(this), amount); uint256 nextLockedBalance = lockedBalance[canonicalToken] + amount; uint256 currentTotalOutstanding = totalOutstanding[canonicalToken]; uint256 currentChainOutstanding = outstandingMinted[canonicalToken][destinationChainSelector]; require( amount <= _availableToMint( canonicalToken, destinationChainSelector, nextLockedBalance, currentTotalOutstanding, currentChainOutstanding ), "CWMultiTokenBridgeL1: exceeds escrow capacity" ); lockedBalance[canonicalToken] = nextLockedBalance; totalOutstanding[canonicalToken] = currentTotalOutstanding + amount; outstandingMinted[canonicalToken][destinationChainSelector] = currentChainOutstanding + amount; if (reserveVerifier != address(0) && !ICWReserveVerifier(reserveVerifier).verifyLock(canonicalToken, destinationChainSelector, amount)) { revert("CWMultiTokenBridgeL1: reserve verification failed"); } emit Locked(canonicalToken, msg.sender, amount); emit OutstandingUpdated( canonicalToken, destinationChainSelector, outstandingMinted[canonicalToken][destinationChainSelector], totalOutstanding[canonicalToken], lockedBalance[canonicalToken] ); 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, destinationChainSelector, recipient, amount); _refundNativeExcess(fee); return messageId; } function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyReceiveRouter { require(!processed[message.messageId], "CWMultiTokenBridgeL1: replayed"); (address canonicalToken, address recipient, uint256 amount) = abi.decode(message.data, (address, address, uint256)); require(canonicalToken != address(0), "CWMultiTokenBridgeL1: zero token"); require(recipient != address(0), "CWMultiTokenBridgeL1: zero recipient"); require(amount > 0, "CWMultiTokenBridgeL1: zero amount"); require(supportedCanonicalToken[canonicalToken], "CWMultiTokenBridgeL1: unsupported token"); require(!paused[canonicalToken], "CWMultiTokenBridgeL1: token paused"); DestinationConfig memory peer = destinations[canonicalToken][message.sourceChainSelector]; require(peer.enabled, "CWMultiTokenBridgeL1: peer disabled"); require(_decodeSender(message.sender) == peer.receiverBridge, "CWMultiTokenBridgeL1: peer mismatch"); require(outstandingMinted[canonicalToken][message.sourceChainSelector] >= amount, "CWMultiTokenBridgeL1: outstanding underflow"); require(totalOutstanding[canonicalToken] >= amount, "CWMultiTokenBridgeL1: total outstanding underflow"); require(lockedBalance[canonicalToken] >= amount, "CWMultiTokenBridgeL1: locked underflow"); processed[message.messageId] = true; outstandingMinted[canonicalToken][message.sourceChainSelector] -= amount; totalOutstanding[canonicalToken] -= amount; lockedBalance[canonicalToken] -= amount; IERC20(canonicalToken).safeTransfer(recipient, amount); emit Released(canonicalToken, recipient, amount); emit OutstandingUpdated( canonicalToken, message.sourceChainSelector, outstandingMinted[canonicalToken][message.sourceChainSelector], totalOutstanding[canonicalToken], lockedBalance[canonicalToken] ); } function withdrawToken(address token, address to, uint256 amount) external onlyAdmin { if (supportedCanonicalToken[token] || lockedBalance[token] > 0) { uint256 currentBalance = IERC20(token).balanceOf(address(this)); require(currentBalance >= lockedBalance[token], "CWMultiTokenBridgeL1: escrow invariant broken"); require(amount <= currentBalance - lockedBalance[token], "CWMultiTokenBridgeL1: amount locked"); } IERC20(token).safeTransfer(to, amount); } function withdrawNative(address payable to, uint256 amount) external onlyAdmin { (bool ok, ) = to.call{value: amount}(""); require(ok, "CWMultiTokenBridgeL1: 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, "CWMultiTokenBridgeL1: 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, "CWMultiTokenBridgeL1: refund failed"); } function _decodeSender(bytes memory senderData) internal pure returns (address senderAddress) { if (senderData.length == 0) { return address(0); } senderAddress = abi.decode(senderData, (address)); } function _availableToMint( address canonicalToken, uint64 destinationChainSelector, uint256 lockedBalanceAmount, uint256 totalOutstandingAmount, uint256 chainOutstanding ) internal view returns (uint256) { if (!supportedCanonicalToken[canonicalToken]) { return 0; } uint256 globalAvailable = lockedBalanceAmount > totalOutstandingAmount ? lockedBalanceAmount - totalOutstandingAmount : 0; uint256 chainCap = maxOutstanding[canonicalToken][destinationChainSelector]; if (chainCap == 0) { return globalAvailable; } if (chainOutstanding >= chainCap) { return 0; } uint256 chainAvailable = chainCap - chainOutstanding; return globalAvailable < chainAvailable ? globalAvailable : chainAvailable; } }