// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IAtomicSettlementAdapter} from "./interfaces/IAtomicSettlementAdapter.sol"; interface ICWMultiTokenBridgeL1Settlement { function calculateFee(address canonicalToken, uint64 destinationChainSelector, address recipient, uint256 amount) external view returns (uint256); function feeToken() external view returns (address); function lockAndSend(address canonicalToken, uint64 destinationChainSelector, address recipient, uint256 amount) external payable returns (bytes32 messageId); } /** * @title CWMultiTokenBridgeSettlementAdapter * @notice Replenishes a fulfiller/vault by forwarding coordinator settlement * funds into CWMultiTokenBridgeL1.lockAndSend. */ contract CWMultiTokenBridgeSettlementAdapter is IAtomicSettlementAdapter { using SafeERC20 for IERC20; ICWMultiTokenBridgeL1Settlement public immutable bridge; uint64 public immutable defaultDestinationSelector; event CwSettlementSent( bytes32 indexed obligationId, bytes32 indexed messageId, address indexed token, address recipient, uint64 destinationSelector, uint256 amount, uint256 fee ); error ZeroAddress(); error FeeTooLow(uint256 requiredFee, uint256 providedFee); error InvalidSettlementData(); constructor(address bridge_, uint64 defaultDestinationSelector_) { if (bridge_ == address(0)) revert ZeroAddress(); bridge = ICWMultiTokenBridgeL1Settlement(bridge_); defaultDestinationSelector = defaultDestinationSelector_; } function executeSettlement( bytes32 obligationId, address token, uint256 amount, address recipient, bytes calldata data ) external payable returns (bytes32 settlementId) { (address settlementRecipient, uint64 destinationSelector, uint256 maxFee) = _decodeSettlementData(recipient, data); uint256 fee = bridge.calculateFee(token, destinationSelector, settlementRecipient, amount); if (maxFee != 0 && fee > maxFee) revert FeeTooLow(fee, maxFee); IERC20(token).forceApprove(address(bridge), amount); address feeToken = bridge.feeToken(); if (feeToken == address(0)) { if (msg.value < fee) revert FeeTooLow(fee, msg.value); settlementId = bridge.lockAndSend{value: fee}(token, destinationSelector, settlementRecipient, amount); _refundNativeExcess(fee); } else { if (IERC20(feeToken).balanceOf(address(this)) < fee) { revert FeeTooLow(fee, IERC20(feeToken).balanceOf(address(this))); } IERC20(feeToken).forceApprove(address(bridge), fee); settlementId = bridge.lockAndSend(token, destinationSelector, settlementRecipient, amount); } emit CwSettlementSent(obligationId, settlementId, token, settlementRecipient, destinationSelector, amount, fee); } function _decodeSettlementData(address fallbackRecipient, bytes calldata data) internal view returns (address settlementRecipient, uint64 destinationSelector, uint256 maxFee) { if (data.length == 0) { settlementRecipient = fallbackRecipient; destinationSelector = defaultDestinationSelector; } else { if (data.length != 96) revert InvalidSettlementData(); (settlementRecipient, destinationSelector, maxFee) = abi.decode(data, (address, uint64, uint256)); if (destinationSelector == 0) { destinationSelector = defaultDestinationSelector; } } if (settlementRecipient == address(0)) revert ZeroAddress(); } function _refundNativeExcess(uint256 feeUsed) internal { if (msg.value <= feeUsed) { return; } (bool ok,) = payable(msg.sender).call{value: msg.value - feeUsed}(""); require(ok, "CWMultiTokenBridgeSettlementAdapter: refund failed"); } }