-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/ccip-develop' into merge-upstream
- Loading branch information
Showing
9 changed files
with
1,556 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
contracts/src/v0.8/ccip/applications/EtherSenderReceiver.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity 0.8.19; | ||
|
||
import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; | ||
|
||
import {IRouterClient} from "../interfaces/IRouterClient.sol"; | ||
import {IWrappedNative} from "../interfaces/IWrappedNative.sol"; | ||
|
||
import {Client} from "./../libraries/Client.sol"; | ||
import {CCIPReceiver} from "./CCIPReceiver.sol"; | ||
|
||
import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; | ||
import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
||
interface CCIPRouter { | ||
function getWrappedNative() external view returns (address); | ||
} | ||
|
||
/// @notice A contract that can send raw ether cross-chain using CCIP. | ||
/// Since CCIP only supports ERC-20 token transfers, this contract accepts | ||
/// normal ether, wraps it, and uses CCIP to send it cross-chain. | ||
/// On the receiving side, the wrapped ether is unwrapped and sent to the final receiver. | ||
/// @notice This contract only supports chains where the wrapped native contract | ||
/// is the WETH contract (i.e not WMATIC, or WAVAX, etc.). This is because the | ||
/// receiving contract will always unwrap the ether using it's local wrapped native contract. | ||
/// @dev This contract is both a sender and a receiver. This same contract can be | ||
/// deployed on source and destination chains to facilitate cross-chain ether transfers | ||
/// and act as a sender and a receiver. | ||
/// @dev This contract is intentionally ownerless and permissionless. This contract | ||
/// will never hold any excess funds, native or otherwise, when used correctly. | ||
contract EtherSenderReceiver is CCIPReceiver, ITypeAndVersion { | ||
using SafeERC20 for IERC20; | ||
|
||
error InvalidTokenAmounts(uint256 gotAmounts); | ||
error InvalidToken(address gotToken, address expectedToken); | ||
error TokenAmountNotEqualToMsgValue(uint256 gotAmount, uint256 msgValue); | ||
error InsufficientMsgValue(uint256 gotAmount, uint256 msgValue); | ||
error InsufficientFee(uint256 gotFee, uint256 fee); | ||
error GasLimitTooLow(uint256 minLimit, uint256 gotLimit); | ||
|
||
string public constant override typeAndVersion = "EtherSenderReceiver 1.5.0-dev"; | ||
|
||
/// @notice The wrapped native token address. | ||
/// @dev If the wrapped native token address changes on the router, this contract will need to be redeployed. | ||
IWrappedNative public immutable i_weth; | ||
|
||
/// @param router The CCIP router address. | ||
constructor(address router) CCIPReceiver(router) { | ||
i_weth = IWrappedNative(CCIPRouter(router).getWrappedNative()); | ||
i_weth.approve(router, type(uint256).max); | ||
} | ||
|
||
/// @notice Need this in order to unwrap correctly. | ||
receive() external payable {} | ||
|
||
/// @notice Get the fee for sending a message to a destination chain. | ||
/// This is mirrored from the router for convenience, construct the appropriate | ||
/// message and get it's fee. | ||
/// @param destinationChainSelector The destination chainSelector | ||
/// @param message The cross-chain CCIP message including data and/or tokens | ||
/// @return fee returns execution fee for the message | ||
/// delivery to destination chain, denominated in the feeToken specified in the message. | ||
/// @dev Reverts with appropriate reason upon invalid message. | ||
function getFee( | ||
uint64 destinationChainSelector, | ||
Client.EVM2AnyMessage calldata message | ||
) external view returns (uint256 fee) { | ||
Client.EVM2AnyMessage memory validatedMessage = _validatedMessage(message); | ||
|
||
return IRouterClient(getRouter()).getFee(destinationChainSelector, validatedMessage); | ||
} | ||
|
||
/// @notice Send raw native tokens cross-chain. | ||
/// @param destinationChainSelector The destination chain selector. | ||
/// @param message The CCIP message with the following fields correctly set: | ||
/// - bytes receiver: The _contract_ address on the destination chain that will receive the wrapped ether. | ||
/// The caller must ensure that this contract address is correct, otherwise funds may be lost forever. | ||
/// - address feeToken: The fee token address. Must be address(0) for native tokens, or a supported CCIP fee token otherwise (i.e, LINK token). | ||
/// In the event a feeToken is set, we will transferFrom the caller the fee amount before sending the message, in order to forward them to the router. | ||
/// - EVMTokenAmount[] tokenAmounts: The tokenAmounts array must contain a single element with the following fields: | ||
/// - uint256 amount: The amount of ether to send. | ||
/// There are a couple of cases here that depend on the fee token specified: | ||
/// 1. If feeToken == address(0), the fee must be included in msg.value. Therefore tokenAmounts[0].amount must be less than msg.value, | ||
/// and the difference will be used as the fee. | ||
/// 2. If feeToken != address(0), the fee is not included in msg.value, and tokenAmounts[0].amount must be equal to msg.value. | ||
/// these fees to the CCIP router. | ||
/// @return messageId The CCIP message ID. | ||
function ccipSend( | ||
uint64 destinationChainSelector, | ||
Client.EVM2AnyMessage calldata message | ||
) external payable returns (bytes32) { | ||
_validateFeeToken(message); | ||
Client.EVM2AnyMessage memory validatedMessage = _validatedMessage(message); | ||
|
||
i_weth.deposit{value: validatedMessage.tokenAmounts[0].amount}(); | ||
|
||
uint256 fee = IRouterClient(getRouter()).getFee(destinationChainSelector, validatedMessage); | ||
if (validatedMessage.feeToken != address(0)) { | ||
// If the fee token is not native, we need to transfer the fee to this contract and re-approve it to the router. | ||
// Its not possible to have any leftover tokens in this path because we transferFrom the exact fee that CCIP | ||
// requires from the caller. | ||
IERC20(validatedMessage.feeToken).safeTransferFrom(msg.sender, address(this), fee); | ||
|
||
// We gave an infinite approval of weth to the router in the constructor. | ||
if (validatedMessage.feeToken != address(i_weth)) { | ||
IERC20(validatedMessage.feeToken).approve(getRouter(), fee); | ||
} | ||
|
||
return IRouterClient(getRouter()).ccipSend(destinationChainSelector, validatedMessage); | ||
} | ||
|
||
// We don't want to keep any excess ether in this contract, so we send over the entire address(this).balance as the fee. | ||
// CCIP will revert if the fee is insufficient, so we don't need to check here. | ||
return IRouterClient(getRouter()).ccipSend{value: address(this).balance}(destinationChainSelector, validatedMessage); | ||
} | ||
|
||
/// @notice Validate the message content. | ||
/// @dev Only allows a single token to be sent. Always overwritten to be address(i_weth) | ||
/// and receiver is always msg.sender. | ||
function _validatedMessage(Client.EVM2AnyMessage calldata message) | ||
internal | ||
view | ||
returns (Client.EVM2AnyMessage memory) | ||
{ | ||
Client.EVM2AnyMessage memory validatedMessage = message; | ||
|
||
if (validatedMessage.tokenAmounts.length != 1) { | ||
revert InvalidTokenAmounts(validatedMessage.tokenAmounts.length); | ||
} | ||
|
||
validatedMessage.data = abi.encode(msg.sender); | ||
validatedMessage.tokenAmounts[0].token = address(i_weth); | ||
|
||
return validatedMessage; | ||
} | ||
|
||
function _validateFeeToken(Client.EVM2AnyMessage calldata message) internal view { | ||
uint256 tokenAmount = message.tokenAmounts[0].amount; | ||
|
||
if (message.feeToken != address(0)) { | ||
// If the fee token is NOT native, then the token amount must be equal to msg.value. | ||
// This is done to ensure that there is no leftover ether in this contract. | ||
if (msg.value != tokenAmount) { | ||
revert TokenAmountNotEqualToMsgValue(tokenAmount, msg.value); | ||
} | ||
} | ||
} | ||
|
||
/// @notice Receive the wrapped ether, unwrap it, and send it to the specified EOA in the data field. | ||
/// @param message The CCIP message containing the wrapped ether amount and the final receiver. | ||
/// @dev The code below should never revert if the message being is valid according | ||
/// to the above _validatedMessage and _validateFeeToken functions. | ||
function _ccipReceive(Client.Any2EVMMessage memory message) internal override { | ||
address receiver = abi.decode(message.data, (address)); | ||
|
||
if (message.destTokenAmounts.length != 1) { | ||
revert InvalidTokenAmounts(message.destTokenAmounts.length); | ||
} | ||
|
||
if (message.destTokenAmounts[0].token != address(i_weth)) { | ||
revert InvalidToken(message.destTokenAmounts[0].token, address(i_weth)); | ||
} | ||
|
||
uint256 tokenAmount = message.destTokenAmounts[0].amount; | ||
i_weth.withdraw(tokenAmount); | ||
|
||
// it is possible that the below call may fail if receiver.code.length > 0 and the contract | ||
// doesn't e.g have a receive() or a fallback() function. | ||
(bool success,) = payable(receiver).call{value: tokenAmount}(""); | ||
if (!success) { | ||
// We have a few options here: | ||
// 1. Revert: this is bad generally because it may mean that these tokens are stuck. | ||
// 2. Store the tokens in a mapping and allow the user to withdraw them with another tx. | ||
// 3. Send WETH to the receiver address. | ||
// We opt for (3) here because at least the receiver will have the funds and can unwrap them if needed. | ||
// However it is worth noting that if receiver is actually a contract AND the contract _cannot_ withdraw | ||
// the WETH, then the WETH will be stuck in this contract. | ||
i_weth.deposit{value: tokenAmount}(); | ||
i_weth.transfer(receiver, tokenAmount); | ||
} | ||
} | ||
} |
Oops, something went wrong.