210 lines
6.5 KiB
Solidity
210 lines
6.5 KiB
Solidity
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
pragma solidity ^0.8.19;
|
||
|
|
|
||
|
|
import "./IRouterClient.sol";
|
||
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @title CCIP Router Implementation
|
||
|
|
* @notice Full Chainlink CCIP Router interface implementation
|
||
|
|
* @dev Implements message sending, fee calculation, and message validation
|
||
|
|
*/
|
||
|
|
contract CCIPRouter is IRouterClient {
|
||
|
|
using SafeERC20 for IERC20;
|
||
|
|
|
||
|
|
// Fee token (LINK token address)
|
||
|
|
address public immutable feeToken;
|
||
|
|
|
||
|
|
// Message tracking
|
||
|
|
mapping(bytes32 => bool) public sentMessages;
|
||
|
|
mapping(bytes32 => bool) public receivedMessages;
|
||
|
|
|
||
|
|
// Chain selectors
|
||
|
|
mapping(uint64 => bool) public supportedChains;
|
||
|
|
mapping(uint64 => address[]) public supportedTokens;
|
||
|
|
|
||
|
|
// Fee configuration
|
||
|
|
uint256 public baseFee; // Base fee in feeToken units
|
||
|
|
uint256 public dataFeePerByte; // Fee per byte of data
|
||
|
|
|
||
|
|
address public admin;
|
||
|
|
|
||
|
|
// Events are inherited from IRouterClient interface
|
||
|
|
|
||
|
|
modifier onlyAdmin() {
|
||
|
|
require(msg.sender == admin, "CCIPRouter: only admin");
|
||
|
|
_;
|
||
|
|
}
|
||
|
|
|
||
|
|
constructor(address _feeToken, uint256 _baseFee, uint256 _dataFeePerByte) {
|
||
|
|
// Allow zero address for native token fees (ETH)
|
||
|
|
// If feeToken is zero, fees are paid in native token (msg.value)
|
||
|
|
feeToken = _feeToken;
|
||
|
|
baseFee = _baseFee;
|
||
|
|
dataFeePerByte = _dataFeePerByte;
|
||
|
|
admin = msg.sender;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Send a message to a destination chain
|
||
|
|
* @param destinationChainSelector The chain selector of the destination chain
|
||
|
|
* @param message The message to send
|
||
|
|
* @return messageId The ID of the sent message
|
||
|
|
* @return fees The fees required for the message
|
||
|
|
*/
|
||
|
|
function ccipSend(
|
||
|
|
uint64 destinationChainSelector,
|
||
|
|
EVM2AnyMessage memory message
|
||
|
|
) external payable returns (bytes32 messageId, uint256 fees) {
|
||
|
|
require(supportedChains[destinationChainSelector], "CCIPRouter: chain not supported");
|
||
|
|
require(message.receiver.length > 0, "CCIPRouter: empty receiver");
|
||
|
|
|
||
|
|
// Calculate fee
|
||
|
|
fees = getFee(destinationChainSelector, message);
|
||
|
|
|
||
|
|
// Collect fee
|
||
|
|
if (fees > 0) {
|
||
|
|
if (feeToken == address(0)) {
|
||
|
|
// Native token (ETH) fees
|
||
|
|
require(msg.value >= fees, "CCIPRouter: insufficient native token fee");
|
||
|
|
} else {
|
||
|
|
// ERC20 token fees
|
||
|
|
IERC20(feeToken).safeTransferFrom(msg.sender, address(this), fees);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate message ID
|
||
|
|
messageId = keccak256(abi.encodePacked(
|
||
|
|
block.chainid,
|
||
|
|
destinationChainSelector,
|
||
|
|
msg.sender,
|
||
|
|
message.receiver,
|
||
|
|
message.data,
|
||
|
|
block.timestamp,
|
||
|
|
block.number
|
||
|
|
));
|
||
|
|
|
||
|
|
require(!sentMessages[messageId], "CCIPRouter: duplicate message");
|
||
|
|
sentMessages[messageId] = true;
|
||
|
|
|
||
|
|
emit MessageSent(
|
||
|
|
messageId,
|
||
|
|
destinationChainSelector,
|
||
|
|
msg.sender,
|
||
|
|
message.receiver,
|
||
|
|
message.data,
|
||
|
|
message.tokenAmounts,
|
||
|
|
message.feeToken,
|
||
|
|
message.extraArgs
|
||
|
|
);
|
||
|
|
|
||
|
|
return (messageId, fees);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Get the fee for sending a message
|
||
|
|
* @param destinationChainSelector The chain selector of the destination chain
|
||
|
|
* @param message The message to send
|
||
|
|
* @return fee The fee required for the message
|
||
|
|
*/
|
||
|
|
function getFee(
|
||
|
|
uint64 destinationChainSelector,
|
||
|
|
EVM2AnyMessage memory message
|
||
|
|
) public view returns (uint256 fee) {
|
||
|
|
require(supportedChains[destinationChainSelector], "CCIPRouter: chain not supported");
|
||
|
|
|
||
|
|
// Base fee
|
||
|
|
fee = baseFee;
|
||
|
|
|
||
|
|
// Data fee (per byte)
|
||
|
|
fee += message.data.length * dataFeePerByte;
|
||
|
|
|
||
|
|
// Token transfer fees
|
||
|
|
for (uint256 i = 0; i < message.tokenAmounts.length; i++) {
|
||
|
|
fee += message.tokenAmounts[i].amount / 1000; // 0.1% of token amount
|
||
|
|
}
|
||
|
|
|
||
|
|
return fee;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Get supported tokens for a destination chain
|
||
|
|
* @param destinationChainSelector The chain selector of the destination chain
|
||
|
|
* @return tokens The list of supported tokens
|
||
|
|
*/
|
||
|
|
function getSupportedTokens(
|
||
|
|
uint64 destinationChainSelector
|
||
|
|
) external view returns (address[] memory tokens) {
|
||
|
|
return supportedTokens[destinationChainSelector];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Add supported chain
|
||
|
|
*/
|
||
|
|
function addSupportedChain(uint64 chainSelector) external onlyAdmin {
|
||
|
|
supportedChains[chainSelector] = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Remove supported chain
|
||
|
|
*/
|
||
|
|
function removeSupportedChain(uint64 chainSelector) external onlyAdmin {
|
||
|
|
supportedChains[chainSelector] = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Add supported token for a chain
|
||
|
|
*/
|
||
|
|
function addSupportedToken(uint64 chainSelector, address token) external onlyAdmin {
|
||
|
|
require(token != address(0), "CCIPRouter: zero token");
|
||
|
|
address[] storage tokens = supportedTokens[chainSelector];
|
||
|
|
for (uint256 i = 0; i < tokens.length; i++) {
|
||
|
|
require(tokens[i] != token, "CCIPRouter: token already supported");
|
||
|
|
}
|
||
|
|
tokens.push(token);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Update fee configuration
|
||
|
|
*/
|
||
|
|
function updateFees(uint256 _baseFee, uint256 _dataFeePerByte) external onlyAdmin {
|
||
|
|
baseFee = _baseFee;
|
||
|
|
dataFeePerByte = _dataFeePerByte;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Change admin
|
||
|
|
*/
|
||
|
|
function changeAdmin(address newAdmin) external onlyAdmin {
|
||
|
|
require(newAdmin != address(0), "CCIPRouter: zero address");
|
||
|
|
admin = newAdmin;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Withdraw collected fees
|
||
|
|
*/
|
||
|
|
function withdrawFees(uint256 amount) external onlyAdmin {
|
||
|
|
if (feeToken == address(0)) {
|
||
|
|
// Native token (ETH) fees
|
||
|
|
payable(admin).transfer(amount);
|
||
|
|
} else {
|
||
|
|
// ERC20 token fees
|
||
|
|
IERC20(feeToken).safeTransfer(admin, amount);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Withdraw all native token (ETH) fees
|
||
|
|
*/
|
||
|
|
function withdrawNativeFees() external onlyAdmin {
|
||
|
|
require(feeToken == address(0), "CCIPRouter: not native token");
|
||
|
|
payable(admin).transfer(address(this).balance);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Receive native token (ETH)
|
||
|
|
*/
|
||
|
|
receive() external payable {}
|
||
|
|
}
|