From 8306ce34ee57b44c7c9bb672204a4c751a46743a Mon Sep 17 00:00:00 2001 From: cqlyj Date: Sun, 1 Dec 2024 22:25:08 +0800 Subject: [PATCH] add TokenTransfer contract to send the token from sepolia to polygon --- src/TokenTransfer.sol | 282 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 src/TokenTransfer.sol diff --git a/src/TokenTransfer.sol b/src/TokenTransfer.sol new file mode 100644 index 0000000..c85c2ba --- /dev/null +++ b/src/TokenTransfer.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IRouterClient} from "@chainlink/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol"; +import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"; +import {Client} from "@chainlink/contracts/src/v0.8/ccip/libraries/Client.sol"; +import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract TokenTransfer is OwnerIsCreator { + using SafeERC20 for IERC20; + + error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner. + error InvalidReceiverAddress(); // Used when the receiver address is 0. + error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance to cover the fees. + error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw. + error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails. + + // Event emitted when the tokens are transferred to an account on another chain. + event TokensTransferred( + bytes32 indexed messageId, // The unique ID of the message. + uint64 indexed destinationChainSelector, // The chain selector of the destination chain. + address receiver, // The address of the receiver on the destination chain. + address token, // The token address that was transferred. + uint256 tokenAmount, // The token amount that was transferred. + address feeToken, // the token address used to pay CCIP fees. + uint256 fees // The fees paid for sending the message. + ); + + // Mapping to keep track of allowlisted destination chains. + mapping(uint64 => bool) public allowlistedChains; + + IRouterClient private s_router; + IERC20 private s_linkToken; + + /// @notice Constructor initializes the contract with the router address. + /// @param _router The address of the router contract. + /// @param _link The address of the link contract. + constructor(address _router, address _link) { + s_router = IRouterClient(_router); + s_linkToken = IERC20(_link); + } + + /// @dev Modifier that checks if the chain with the given destinationChainSelector is allowlisted. + /// @param _destinationChainSelector The selector of the destination chain. + modifier onlyAllowlistedChain(uint64 _destinationChainSelector) { + if (!allowlistedChains[_destinationChainSelector]) + revert DestinationChainNotAllowlisted(_destinationChainSelector); + _; + } + + /// @dev Modifier that checks the receiver address is not 0. + /// @param _receiver The receiver address. + modifier validateReceiver(address _receiver) { + if (_receiver == address(0)) revert InvalidReceiverAddress(); + _; + } + + /// @dev Updates the allowlist status of a destination chain for transactions. + /// @notice This function can only be called by the owner. + /// @param _destinationChainSelector The selector of the destination chain to be updated. + /// @param allowed The allowlist status to be set for the destination chain. + function allowlistDestinationChain( + uint64 _destinationChainSelector, + bool allowed + ) external onlyOwner { + allowlistedChains[_destinationChainSelector] = allowed; + } + + /// @notice Transfer tokens to receiver on the destination chain. + /// @notice pay in LINK. + /// @notice the token must be in the list of supported tokens. + /// @notice This function can only be called by the owner. + /// @dev Assumes your contract has sufficient LINK tokens to pay for the fees. + /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain. + /// @param _receiver The address of the recipient on the destination blockchain. + /// @param _token token address. + /// @param _amount token amount. + /// @return messageId The ID of the message that was sent. + function transferTokensPayLINK( + uint64 _destinationChainSelector, + address _receiver, + address _token, + uint256 _amount + ) + external + onlyOwner + onlyAllowlistedChain(_destinationChainSelector) + validateReceiver(_receiver) + returns (bytes32 messageId) + { + // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message + // address(linkToken) means fees are paid in LINK + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, + _token, + _amount, + address(s_linkToken) + ); + + // Get the fee required to send the message + uint256 fees = s_router.getFee( + _destinationChainSelector, + evm2AnyMessage + ); + + if (fees > s_linkToken.balanceOf(address(this))) + revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees); + + // approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK + s_linkToken.approve(address(s_router), fees); + + // approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token + IERC20(_token).approve(address(s_router), _amount); + + // Send the message through the router and store the returned message ID + messageId = s_router.ccipSend( + _destinationChainSelector, + evm2AnyMessage + ); + + // Emit an event with message details + emit TokensTransferred( + messageId, + _destinationChainSelector, + _receiver, + _token, + _amount, + address(s_linkToken), + fees + ); + + // Return the message ID + return messageId; + } + + /// @notice Transfer tokens to receiver on the destination chain. + /// @notice Pay in native gas such as ETH on Ethereum or POL on Polygon. + /// @notice the token must be in the list of supported tokens. + /// @notice This function can only be called by the owner. + /// @dev Assumes your contract has sufficient native gas like ETH on Ethereum or POL on Polygon. + /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain. + /// @param _receiver The address of the recipient on the destination blockchain. + /// @param _token token address. + /// @param _amount token amount. + /// @return messageId The ID of the message that was sent. + function transferTokensPayNative( + uint64 _destinationChainSelector, + address _receiver, + address _token, + uint256 _amount + ) + external + onlyOwner + onlyAllowlistedChain(_destinationChainSelector) + validateReceiver(_receiver) + returns (bytes32 messageId) + { + // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message + // address(0) means fees are paid in native gas + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, + _token, + _amount, + address(0) + ); + + // Get the fee required to send the message + uint256 fees = s_router.getFee( + _destinationChainSelector, + evm2AnyMessage + ); + + if (fees > address(this).balance) + revert NotEnoughBalance(address(this).balance, fees); + + // approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token + IERC20(_token).approve(address(s_router), _amount); + + // Send the message through the router and store the returned message ID + messageId = s_router.ccipSend{value: fees}( + _destinationChainSelector, + evm2AnyMessage + ); + + // Emit an event with message details + emit TokensTransferred( + messageId, + _destinationChainSelector, + _receiver, + _token, + _amount, + address(0), + fees + ); + + // Return the message ID + return messageId; + } + + /// @notice Construct a CCIP message. + /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for tokens transfer. + /// @param _receiver The address of the receiver. + /// @param _token The token to be transferred. + /// @param _amount The amount of the token to be transferred. + /// @param _feeTokenAddress The address of the token used for fees. Set address(0) for native gas. + /// @return Client.EVM2AnyMessage Returns an EVM2AnyMessage struct which contains information for sending a CCIP message. + function _buildCCIPMessage( + address _receiver, + address _token, + uint256 _amount, + address _feeTokenAddress + ) private pure returns (Client.EVM2AnyMessage memory) { + // Set the token amounts + Client.EVMTokenAmount[] + memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: _token, + amount: _amount + }); + + // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message + return + Client.EVM2AnyMessage({ + receiver: abi.encode(_receiver), // ABI-encoded receiver address + data: "", // No data + tokenAmounts: tokenAmounts, // The amount and type of token being transferred + extraArgs: Client._argsToBytes( + // Additional arguments, setting gas limit and allowing out-of-order execution. + // Best Practice: For simplicity, the values are hardcoded. It is advisable to use a more dynamic approach + // where you set the extra arguments off-chain. This allows adaptation depending on the lanes, messages, + // and ensures compatibility with future CCIP upgrades. Read more about it here: https://docs.chain.link/ccip/best-practices#using-extraargs + Client.EVMExtraArgsV2({ + gasLimit: 0, // Gas limit for the callback on the destination chain + allowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages from the same sender + }) + ), + // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees + feeToken: _feeTokenAddress + }); + } + + /// @notice Fallback function to allow the contract to receive Ether. + /// @dev This function has no function body, making it a default function for receiving Ether. + /// It is automatically called when Ether is transferred to the contract without any data. + receive() external payable {} + + /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract. + /// @dev This function reverts if there are no funds to withdraw or if the transfer fails. + /// It should only be callable by the owner of the contract. + /// @param _beneficiary The address to which the Ether should be transferred. + function withdraw(address _beneficiary) public onlyOwner { + // Retrieve the balance of this contract + uint256 amount = address(this).balance; + + // Revert if there is nothing to withdraw + if (amount == 0) revert NothingToWithdraw(); + + // Attempt to send the funds, capturing the success status and discarding any return data + (bool sent, ) = _beneficiary.call{value: amount}(""); + + // Revert if the send failed, with information about the attempted transfer + if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount); + } + + /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token. + /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw. + /// @param _beneficiary The address to which the tokens will be sent. + /// @param _token The contract address of the ERC20 token to be withdrawn. + function withdrawToken( + address _beneficiary, + address _token + ) public onlyOwner { + // Retrieve the balance of this contract + uint256 amount = IERC20(_token).balanceOf(address(this)); + + // Revert if there is nothing to withdraw + if (amount == 0) revert NothingToWithdraw(); + + IERC20(_token).safeTransfer(_beneficiary, amount); + } +}