// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.24; import {ICrossChainVerifierV1} from "../interfaces/ICrossChainVerifierV1.sol"; import {IBridgeV3} from "../interfaces/lombard/IBridgeV3.sol"; import {IMailbox} from "../interfaces/lombard/IMailbox.sol"; import {FeeTokenHandler} from "../libraries/FeeTokenHandler.sol"; import {Internal} from "../libraries/Internal.sol"; import {MessageV1Codec} from "../libraries/MessageV1Codec.sol"; import {BaseVerifier} from "./components/BaseVerifier.sol"; import {Ownable2StepMsgSender} from "@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol"; import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol"; import {EnumerableMap} from "@openzeppelin/contracts@5.3.0/utils/structs/EnumerableMap.sol"; import {EnumerableSet} from "@openzeppelin/contracts@5.3.0/utils/structs/EnumerableSet.sol"; contract LombardVerifier is BaseVerifier, Ownable2StepMsgSender { using EnumerableMap for EnumerableMap.AddressToAddressMap; using EnumerableSet for EnumerableSet.UintSet; using SafeERC20 for IERC20; error ZeroBridge(); error ZeroLombardChainId(); error ZeroAllowedCaller(); error PathNotExist(uint64 remoteChainSelector); error ExecutionError(); error InvalidMessageLength(uint256 expected, uint256 actual); error InvalidMessageId(bytes32 messageMessageId, bytes32 bridgeMessageId); error InvalidReceiver(bytes); error InvalidMessageVersion(uint8 expected, uint8 actual); error InvalidCCVVersion(bytes4 expected, bytes4 actual); error TokenNotSupported(address token); error MustTransferTokens(); error InvalidVerifierResults(); error InvalidToken(bytes32 expected, bytes32 actual); error InvalidSender(bytes32 expected, bytes32 actual); error InvalidAmount(uint256 expected, uint256 actual); error RemoteTokenOrAdapterMismatch(bytes32 bridgeToken, bytes32 remoteToken, bytes32 remoteAdapter); /// @param remoteChainSelector CCIP selector of destination chain. /// @param lChainId The chain id of destination chain by Lombard Multi Chain Id conversion. /// @param allowedCaller The address of TokenPool on destination chain allowed to handle GMP message. event PathSet(uint64 indexed remoteChainSelector, bytes32 indexed lChainId, bytes32 allowedCaller); /// @param remoteChainSelector CCIP selector of destination chain. /// @param lChainId The chain id of destination chain by Lombard Multi Chain Id conversion. /// @param allowedCaller The address that's allowed to call the bridge on the destination chain. event PathRemoved(uint64 indexed remoteChainSelector, bytes32 indexed lChainId, bytes32 allowedCaller); event SupportedTokenRemoved(address token); event SupportedTokenSet(address localToken, address localAdapter); event RemoteAdapterSet(uint64 indexed remoteChainSelector, address indexed token, bytes32 remoteAdapter); event DynamicConfigSet(DynamicConfig dynamicConfig); struct DynamicConfig { address feeAggregator; // Address to which fees are withdrawn. } struct Path { /// @notice The address that's allowed to call the bridge on the destination chain. bytes32 allowedCaller; /// @notice Lombard chain id of destination chain. bytes32 lChainId; } struct RemoteAdapterArgs { /// @notice CCIP chain selector of remote chain. uint64 remoteChainSelector; /// @notice The local token address. address token; /// @notice The remote adapter token identifier accepted by the bridge. bytes32 remoteAdapter; } struct SupportedTokenArgs { /// @notice The local token address. address localToken; /// @notice The local adapter address. Can be zero address if no adapter is used. address localAdapter; } string public constant typeAndVersion = "LombardVerifier 2.0.0"; /// @notice The size of the version tag in bytes. uint256 private constant VERSION_TAG_SIZE = 4; /// @notice The size of a bytes32 in bytes. uint256 private constant BYTES32_SIZE = 32; /// @notice The expected size of the bridged message (version tag + message ID). uint256 private constant BRIDGED_MESSAGE_SIZE = VERSION_TAG_SIZE + BYTES32_SIZE; /// @notice The size of the rawPayload length field in ccvData. uint256 private constant RAW_PAYLOAD_LENGTH_SIZE = 2; uint256 private constant PAYLOAD_START_INDEX = VERSION_TAG_SIZE + RAW_PAYLOAD_LENGTH_SIZE; /// @notice Supported bridge message version. uint8 internal constant SUPPORTED_BRIDGE_MSG_VERSION = 2; /// @notice The address of bridge contract. IBridgeV3 public immutable i_bridge; /// @notice Mapping of supported tokens to adapters, where adapters may be address(0). Even if an adapter is used, the /// source token must be added to the supported tokens set. EnumerableMap.AddressToAddressMap internal s_supportedTokens; /// @notice Set of supported chains for cross-chain transfers. EnumerableSet.UintSet internal s_supportedChains; /// @notice Mapping of CCIP chain selector to chain specific config. mapping(uint64 chainSelector => Path path) internal s_chainSelectorToPath; /// @notice Mapping of (remote chain selector, local token) to the optional remote adapter token identifier. mapping(uint64 remoteChainSelector => mapping(address token => bytes32 remoteAdapter)) internal s_remoteAdapters; DynamicConfig private s_dynamicConfig; constructor( DynamicConfig memory dynamicConfig, IBridgeV3 bridge, string[] memory storageLocation, address rmn, bytes4 versionTag ) BaseVerifier(storageLocation, rmn, versionTag) { _setDynamicConfig(dynamicConfig); if (address(bridge) == address(0)) { revert ZeroBridge(); } uint8 bridgeMsgVersion = bridge.MSG_VERSION(); if (bridgeMsgVersion != SUPPORTED_BRIDGE_MSG_VERSION) { revert InvalidMessageVersion(SUPPORTED_BRIDGE_MSG_VERSION, bridgeMsgVersion); } i_bridge = bridge; } /// @notice Returns the dynamic config. function getDynamicConfig() external view returns (DynamicConfig memory) { return s_dynamicConfig; } /// @notice Sets the dynamic config. /// @param dynamicConfig The dynamic configuration. /// @dev FeeTokenHandler will revert if feeAggregator is zero when withdrawing fees. /// @dev A zero address fee aggregator is valid, and intentionally reverts calls to withdraw fee tokens. function setDynamicConfig( DynamicConfig memory dynamicConfig ) external onlyOwner { _setDynamicConfig(dynamicConfig); } /// @dev FeeTokenHandler will revert if feeAggregator is zero when withdrawing fees. /// @dev A zero address fee aggregator is valid, and intentionally reverts calls to withdraw fee tokens. function _setDynamicConfig( DynamicConfig memory dynamicConfig ) internal { s_dynamicConfig = dynamicConfig; emit DynamicConfigSet(dynamicConfig); } /// @inheritdoc ICrossChainVerifierV1 function forwardToVerifier( MessageV1Codec.MessageV1 calldata message, bytes32 messageId, address, uint256, bytes calldata ) external returns (bytes memory verifierData) { _assertNotCursedByRMN(message.destChainSelector); // We only support token transfers. if (message.tokenTransfer.length == 0) { revert MustTransferTokens(); } // Sender must be an abi encoded EVM address. _assertSenderIsAllowed(message.destChainSelector, abi.decode(message.sender, (address))); return _callDepositOnBridge(message.tokenTransfer[0], message.destChainSelector, message.sender, messageId); } function _callDepositOnBridge( MessageV1Codec.TokenTransferV1 calldata tokenTransfer, uint64 destChainSelector, bytes calldata sender, bytes32 messageId ) internal returns (bytes memory) { // The Lombard bridge assumes addresses fit in 32 bytes and therefore only supports up to 32 byte addresses. if (tokenTransfer.tokenReceiver.length > 32) { revert InvalidReceiver(tokenTransfer.tokenReceiver); } // Check if the token is supported. This CCV will only support Lombard tokens. address sourceToken = abi.decode(tokenTransfer.sourceTokenAddress, (address)); if (!s_supportedTokens.contains(sourceToken)) { revert TokenNotSupported(sourceToken); } Path memory path = s_chainSelectorToPath[destChainSelector]; if (path.allowedCaller == bytes32(0)) { revert PathNotExist(destChainSelector); } // For some tokens we need to override the source token with an adapter. address bridgeTokenOrAdapter; { address localAdapter = s_supportedTokens.get(sourceToken); bridgeTokenOrAdapter = localAdapter != address(0) ? localAdapter : sourceToken; bytes32 remoteTokenOrAdapter = i_bridge.getAllowedDestinationToken(path.lChainId, bridgeTokenOrAdapter); bytes32 expectedDestToken = Internal._leftPadBytesToBytes32(tokenTransfer.destTokenAddress); if (remoteTokenOrAdapter != expectedDestToken) { bytes32 remoteAdapter = s_remoteAdapters[destChainSelector][sourceToken]; if (remoteAdapter == bytes32(0) || remoteTokenOrAdapter != remoteAdapter) { revert RemoteTokenOrAdapterMismatch(remoteTokenOrAdapter, expectedDestToken, remoteAdapter); } } } (, bytes32 payloadHash) = i_bridge.deposit({ destinationChain: path.lChainId, token: bridgeTokenOrAdapter, sender: abi.decode(sender, (address)), // Left pad receiver to 32 bytes if not already 32 bytes. recipient: Internal._leftPadBytesToBytes32(tokenTransfer.tokenReceiver), amount: tokenTransfer.amount, destinationCaller: path.allowedCaller, optionalMessage: bytes.concat(versionTag(), messageId) }); // Return raw bytes instead of abi.encode for gas efficiency. return bytes.concat(payloadHash); } /// @inheritdoc ICrossChainVerifierV1 /// @dev ccvData format: /// [versionTag (4 bytes)][rawPayloadLength (2 bytes)][rawPayload (variable)][proofLength (2 bytes)][proof (variable)] function verifyMessage( MessageV1Codec.MessageV1 calldata message, bytes32 messageId, bytes calldata ccvData ) external { _assertNotCursedByRMN(message.sourceChainSelector); _onlyOffRamp(message.sourceChainSelector); { bytes4 versionPrefix = bytes4(ccvData[:VERSION_TAG_SIZE]); if (versionPrefix != versionTag()) { revert InvalidCCVVersion(versionTag(), versionPrefix); } } if (ccvData.length < PAYLOAD_START_INDEX) { revert InvalidVerifierResults(); } uint256 rawPayloadLength = uint16(bytes2(ccvData[VERSION_TAG_SIZE:PAYLOAD_START_INDEX])); if (ccvData.length < PAYLOAD_START_INDEX + rawPayloadLength + RAW_PAYLOAD_LENGTH_SIZE) { revert InvalidVerifierResults(); } uint256 proofDataStartIndex = PAYLOAD_START_INDEX + rawPayloadLength; _validatePayload( ccvData[PAYLOAD_START_INDEX:proofDataStartIndex], // rawPayload message.sender, message.tokenTransfer[0].destTokenAddress, message.tokenTransfer[0].tokenReceiver, message.tokenTransfer[0].amount ); { uint256 proofLength = uint16(bytes2(ccvData[proofDataStartIndex:proofDataStartIndex + RAW_PAYLOAD_LENGTH_SIZE])); uint256 proofStartIndex = proofDataStartIndex + RAW_PAYLOAD_LENGTH_SIZE; if (ccvData.length < proofStartIndex + proofLength) { revert InvalidVerifierResults(); } (, bool executed, bytes memory bridgedMessage) = IMailbox(i_bridge.mailbox()) .deliverAndHandle( ccvData[PAYLOAD_START_INDEX:proofDataStartIndex], // rawPayload ccvData[proofStartIndex:proofStartIndex + proofLength] // proof ); if (!executed) { revert ExecutionError(); } if (bridgedMessage.length != BRIDGED_MESSAGE_SIZE) { revert InvalidMessageLength(BRIDGED_MESSAGE_SIZE, bridgedMessage.length); } bytes4 version; bytes32 returnedMessageId; assembly { version := mload(add(bridgedMessage, 0x20)) returnedMessageId := mload(add(bridgedMessage, 0x24)) } if (version != versionTag()) { revert InvalidCCVVersion(versionTag(), version); } if (returnedMessageId != messageId) { revert InvalidMessageId(messageId, returnedMessageId); } } } function _validatePayload( bytes calldata rawPayload, bytes calldata expectedSender, bytes calldata expectedToken, bytes calldata expectedReceiver, uint256 expectedAmount ) internal view { bytes32 rawToToken; bytes32 rawSender; bytes32 rawRecipient; uint256 amount; { (,,,,, bytes memory msgBody) = abi.decode(rawPayload[4:], (bytes32, uint256, bytes32, address, address, bytes)); assembly { rawToToken := mload(add(msgBody, 0x21)) // bytes 1..32 rawSender := mload(add(msgBody, 0x41)) // bytes 33..64 rawRecipient := mload(add(msgBody, 0x61)) // bytes 65..96 amount := mload(add(msgBody, 0x81)) // bytes 97..128 } } { bytes32 expected = Internal._leftPadBytesToBytes32(expectedToken); // When a local adapter is configured, the bridge payload encodes the adapter address // instead of the local token address. address localToken = address(bytes20(expectedToken)); if (s_supportedTokens.contains(localToken)) { address localAdapter = s_supportedTokens.get(localToken); if (localAdapter != address(0)) { expected = Internal._leftPadBytesToBytes32(abi.encodePacked(localAdapter)); } } if (rawToToken != expected) { revert InvalidToken(expected, rawToToken); } } { bytes32 expected = Internal._leftPadBytesToBytes32(expectedSender); if (rawSender != expected) { revert InvalidSender(expected, rawSender); } } if (rawRecipient != Internal._leftPadBytesToBytes32(expectedReceiver)) { revert InvalidReceiver(expectedReceiver); } if (amount != expectedAmount) { revert InvalidAmount(expectedAmount, amount); } } /// @notice Gets the list of supported tokens for cross-chain transfers. function getSupportedTokens() external view returns (address[] memory) { return s_supportedTokens.keys(); } /// @notice Checks if a token is supported for cross-chain transfers. /// @param token The token address to check. /// @return True if the token is supported, false otherwise. function isSupportedToken( address token ) external view returns (bool) { return s_supportedTokens.contains(token); } /// @notice Update the supported tokens for cross-chain transfers. When adding a token, if no adapter is set it /// approves the bridge to spend the token. If an adapter is set, it approves the adapter to spend the token. /// When removing a token, it resets the corresponding allowance to zero. /// @param tokensToRemove Array of token addresses to remove from supported tokens. /// @param tokensToSet Array of token addresses to set to supported tokens. function updateSupportedTokens( address[] calldata tokensToRemove, SupportedTokenArgs[] calldata tokensToSet ) external onlyOwner { for (uint256 i = 0; i < tokensToRemove.length; ++i) { address tokenToRemove = tokensToRemove[i]; address adapter = s_supportedTokens.get(tokenToRemove); if (s_supportedTokens.remove(tokenToRemove)) { // If adapter exists, reset token->adapter allowance. Otherwise reset token->bridge allowance. if (adapter != address(0)) { IERC20(tokenToRemove).forceApprove(adapter, 0); } else { IERC20(tokenToRemove).forceApprove(address(i_bridge), 0); } emit SupportedTokenRemoved(tokenToRemove); } } for (uint256 i = 0; i < tokensToSet.length; ++i) { SupportedTokenArgs memory tokenToAdd = tokensToSet[i]; // If the token already exists and the adapter is changing, revoke the old approval. if (s_supportedTokens.contains(tokenToAdd.localToken)) { address oldAdapter = s_supportedTokens.get(tokenToAdd.localToken); if (oldAdapter != tokenToAdd.localAdapter) { if (oldAdapter != address(0)) { IERC20(tokenToAdd.localToken).forceApprove(oldAdapter, 0); } else { IERC20(tokenToAdd.localToken).forceApprove(address(i_bridge), 0); } } } s_supportedTokens.set(tokenToAdd.localToken, tokenToAdd.localAdapter); // If adapter exists, approve token->adapter for adapter-mediated burn/bridge flow. if (tokenToAdd.localAdapter != address(0)) { IERC20(tokenToAdd.localToken).forceApprove(tokenToAdd.localAdapter, type(uint256).max); } else { IERC20(tokenToAdd.localToken).forceApprove(address(i_bridge), type(uint256).max); } emit SupportedTokenSet(tokenToAdd.localToken, tokenToAdd.localAdapter); } } /// @notice Returns the list of supported chains. /// @return Array of supported CCIP chain selectors. function getSupportedChains() external view returns (uint64[] memory) { uint256 length = s_supportedChains.length(); uint64[] memory chains = new uint64[](length); for (uint256 i = 0; i < length; ++i) { chains[i] = uint64(s_supportedChains.at(i)); } return chains; } /// @notice Gets the path for a given CCIP chain selector. /// @param remoteChainSelector CCIP chain selector of remote chain. /// @return Path struct containing lChainId and allowedCaller. function getPath( uint64 remoteChainSelector ) external view returns (Path memory) { return s_chainSelectorToPath[remoteChainSelector]; } /// @notice Gets the remote adapter for a given remote chain and local token. /// @param remoteChainSelector CCIP chain selector of remote chain. /// @param token The local token address. /// @return The remote adapter token identifier, or bytes32(0) if not set. function getRemoteAdapter( uint64 remoteChainSelector, address token ) external view returns (bytes32) { return s_remoteAdapters[remoteChainSelector][token]; } /// @notice Sets the lChainId and allowed caller for a CCIP chain selector. /// @param remoteChainSelector CCIP chain selector of remote chain. /// @param lChainId Lombard chain id of remote chain. /// @param allowedCaller The destination caller bytes. Must fit in 32 bytes and is left-padded for storage. function setPath( uint64 remoteChainSelector, bytes32 lChainId, bytes calldata allowedCaller ) external onlyOwner { if (lChainId == bytes32(0)) { revert ZeroLombardChainId(); } bytes32 leftPaddedAllowedCaller = Internal._leftPadBytesToBytes32(allowedCaller); if (leftPaddedAllowedCaller == bytes32(0)) { revert ZeroAllowedCaller(); } s_chainSelectorToPath[remoteChainSelector] = Path({lChainId: lChainId, allowedCaller: leftPaddedAllowedCaller}); s_supportedChains.add(uint256(remoteChainSelector)); emit PathSet(remoteChainSelector, lChainId, leftPaddedAllowedCaller); } /// @notice Sets remote adapter token identifiers for (remote chain, token) pairs. /// @param remoteAdapterArgs Array of remote adapter configurations to set. function setRemoteAdapters( RemoteAdapterArgs[] calldata remoteAdapterArgs ) external onlyOwner { for (uint256 i = 0; i < remoteAdapterArgs.length; ++i) { RemoteAdapterArgs calldata args = remoteAdapterArgs[i]; s_remoteAdapters[args.remoteChainSelector][args.token] = args.remoteAdapter; emit RemoteAdapterSet(args.remoteChainSelector, args.token, args.remoteAdapter); } } /// @notice Removes the path for the given CCIP chain selectors. This disables any traffic to those chains. /// @param remoteChainSelectors CCIP chain selectors of destination chains. function removePaths( uint64[] memory remoteChainSelectors ) external onlyOwner { for (uint256 i = 0; i < remoteChainSelectors.length; ++i) { uint64 remoteChainSelector = remoteChainSelectors[i]; Path memory path = s_chainSelectorToPath[remoteChainSelector]; if (!s_supportedChains.remove(uint256(remoteChainSelector))) { revert PathNotExist(remoteChainSelector); } delete s_chainSelectorToPath[remoteChainSelector]; emit PathRemoved(remoteChainSelector, path.lChainId, path.allowedCaller); } } function applyRemoteChainConfigUpdates( RemoteChainConfigArgs[] calldata remoteChainConfigArgs ) external onlyOwner { _applyRemoteChainConfigUpdates(remoteChainConfigArgs); } /// @notice Updates senders that are allowed to use this verifier. /// @param allowlistConfigArgsItems Array of AllowListConfigArgs, where each item is for a destChainSelector. function applyAllowlistUpdates( AllowlistConfigArgs[] calldata allowlistConfigArgsItems ) external onlyOwner { _applyAllowlistUpdates(allowlistConfigArgsItems); } /// @notice Sets the finality config according to the FinalityCodec library encoding. /// @param allowedFinality The finality settings allowed by this verifier. function setAllowedFinalityConfig( bytes4 allowedFinality ) external onlyOwner { _setAllowedFinalityConfig(allowedFinality); } /// @notice Updates the storage location identifiers. /// @param newLocations The new storage location identifiers. function updateStorageLocations( string[] memory newLocations ) external onlyOwner { _setStorageLocations(newLocations); } // ================================================================ // │ Fees │ // ================================================================ /// @notice Withdraws the outstanding fee token balances to the fee aggregator. /// @param feeTokens The fee tokens to withdraw. /// @dev This function can be permissionless as it only transfers tokens to the fee aggregator which is a trusted address. function withdrawFeeTokens( address[] calldata feeTokens ) external { FeeTokenHandler._withdrawFeeTokens(feeTokens, s_dynamicConfig.feeAggregator); } }