diff --git a/.gitmodules b/.gitmodules index 86d572d1..77f395f7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "lib/PermitC"] path = lib/PermitC url = https://github.com/limitbreakinc/PermitC +[submodule "lib/chainlink.git"] + path = lib/chainlink.git + url = https://github.com/smartcontractkit/chainlink.git diff --git a/lib/chainlink.git b/lib/chainlink.git new file mode 160000 index 00000000..0187f18b --- /dev/null +++ b/lib/chainlink.git @@ -0,0 +1 @@ +Subproject commit 0187f18ba62b44d4c8ff20f07ef8dfd6e0d7b451 diff --git a/remappings.txt b/remappings.txt index 413cecc2..fc11654b 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,3 +5,4 @@ forge-std/=lib/forge-std/src/ @erc721a-upgradeable/=lib/ERC721A-Upgradeable/contracts/ @limitbreak/creator-token-standards/=lib/creator-token-standards/src/ @limitbreak/permit-c/=lib/PermitC/src/ +@chainlink/=lib/chainlink.git/contracts/src/v0.8/ diff --git a/src/interface/ICrosschain.sol b/src/interface/ICrosschain.sol deleted file mode 100644 index b8de6212..00000000 --- a/src/interface/ICrosschain.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -interface ICrosschain { - - /** - * @notice Sends a cross-chain transaction. - * @param _destinationChain The destination chain ID. - * @param _callAddress The address of the contract on the destination chain. - * @param _payload The payload to send to the destination chain. - * @param _extraArgs The extra arguments to pass - * @dev extraArgs may contain items such as token, amount, feeTokenAddress, receipient, gasLimit, etc - */ - function sendCrossChainTransaction( - uint64 _destinationChain, - address _callAddress, - bytes calldata _payload, - bytes calldata _extraArgs - ) external payable; - - /** - * @notice callback function for when a cross-chain transaction is sent. - * @param _destinationChain The destination chain ID. - * @param _callAddress The address of the contract on the destination chain. - * @param _payload The payload sent to the destination chain. - * @param _extraArgs The extra arguments sent to the callAddress on the destination chain. - */ - function onCrossChainTransactionSent( - uint64 _destinationChain, - address _callAddress, - bytes calldata _payload, - bytes calldata _extraArgs - ) internal; - - /** - * @notice callback function for when a cross-chain transaction is received. - * @param _sourceChain The source chain ID. - * @param _sourceAddress The address of the contract on the source chain. - * @param _payload The payload sent to the destination chain. - * @param _extraArgs The extra arguments sent to the callAddress on the destination chain. - */ - function onCrossChainTransactionReceived( - uint64 _sourceChain, - address _sourceAddress, - bytes calldata _payload, - bytes calldata _extraArgs - ) internal; - - function setRouter(address _router) external; - function getRouter() external view returns (address); - -} diff --git a/src/module/token/crosschain/chainlink.sol b/src/module/token/crosschain/chainlink.sol new file mode 100644 index 00000000..c1f7fee5 --- /dev/null +++ b/src/module/token/crosschain/chainlink.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Module} from "../../../Module.sol"; + +import {Role} from "../../../Role.sol"; + +import {IERC20} from "../../../interface/IERC20.sol"; +import {IInstallationCallback} from "../../../interface/IInstallationCallback.sol"; +import {CrossChain} from "./CrossChain.sol"; +import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; + +import {CCIPReceiver} from "@chainlink/ccip/applications/CCIPReceiver.sol"; +import {IRouterClient} from "@chainlink/ccip/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/ccip/libraries/Client.sol"; + +library ChainlinkCrossChainStorage { + + /// @custom:storage-location erc7201:token.minting.chainlinkcrosschain + bytes32 public constant CHAINLINKCROSSCHAIN_STORAGE_POSITION = keccak256( + abi.encode(uint256(keccak256("token.minting.chainlinkcrosschain.erc721")) - 1) + ) & ~bytes32(uint256(0xff)); + + struct Data { + address router; + address linkToken; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = CHAINLINKCROSSCHAIN_STORAGE_POSITION; + assembly { + data_.slot := position + } + } + +} + +contract ChainlinkCrossChain is Module, CrossChain, CCIPReceiver { + + error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); + + constructor(address _router, address _link) CCIPReceiver(_router) {} + + /*////////////////////////////////////////////////////////////// + MODULE CONFIG + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns all implemented callback and fallback functions. + function getModuleConfig() external pure override returns (ModuleConfig memory config) { + config.fallbackFunctions = new FallbackFunction[](5); + + config.fallbackFunctions[0] = FallbackFunction({selector: this.getRouter.selector, permissionBits: 0}); + config.fallbackFunctions[1] = FallbackFunction({selector: this.getLinkToken.selector, permissionBits: 0}); + config.fallbackFunctions[2] = + FallbackFunction({selector: this.setRouter.selector, permissionBits: Role._MANAGER_ROLE}); + config.fallbackFunctions[3] = + FallbackFunction({selector: this.setLinkToken.selector, permissionBits: Role._MANAGER_ROLE}); + config.fallbackFunctions[4] = + FallbackFunction({selector: this.sendCrossChainTransaction.selector, permissionBits: 0}); + + config.registerInstallationCallback = true; + } + + /*////////////////////////////////////////////////////////////// + INSTALL / UNINSTALL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Called by a Core into an Module during the installation of the Module. + function onInstall(bytes calldata data) external { + (address router, address linkToken) = abi.decode(data, (address, address)); + _chainlinkCrossChainStorage().router = router; + _chainlinkCrossChainStorage().linkToken = linkToken; + } + + /// @dev Called by a Core into an Module during the uninstallation of the Module. + function onUninstall(bytes calldata data) external {} + + /// @dev Returns bytes encoded install params, to be sent to `onInstall` function + function encodeBytesOnInstall(address router, address linkToken) external pure returns (bytes memory) { + return abi.encode(router, linkToken); + } + + /// @dev Returns bytes encoded uninstall params, to be sent to `onUninstall` function + function encodeBytesOnUninstall() external pure returns (bytes memory) { + return ""; + } + + /*////////////////////////////////////////////////////////////// + FALLBACK FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function getRouter() public view override(CrossChain, CCIPReceiver) returns (address) { + return _chainlinkCrossChainStorage().router; + } + + function getLinkToken() external view returns (address) { + return _chainlinkCrossChainStorage().linkToken; + } + + function setRouter(address router) external override { + _chainlinkCrossChainStorage().router = router; + } + + function setLinkToken(address linkToken) external { + _chainlinkCrossChainStorage().linkToken = linkToken; + } + + function sendCrossChainTransaction( + uint64 _destinationChain, + address _callAddress, + bytes calldata _data, + bytes calldata _extraArgs + ) external payable override { + ( + address _recipient, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes memory ccipMessageExtraArgs + ) = abi.decode(_extraArgs, (address, address, uint256, address, bytes)); + + if (_feeTokenAddress == address(0)) { + _sendMessagePayNative(_destinationChain, _recipient, _data, _token, _amount, ccipMessageExtraArgs); + } else { + _sendMessagePayToken( + _destinationChain, _recipient, _data, _token, _amount, _feeTokenAddress, ccipMessageExtraArgs + ); + } + + onCrossChainTransactionSent(_destinationChain, _callAddress, _data, _extraArgs); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function onCrossChainTransactionSent( + uint64 _destinationChain, + address _callAddress, + bytes calldata _payload, + bytes calldata _extraArgs + ) internal override { + /// post cross chain transaction sent logic goes here + } + + function onCrossChainTransactionReceived( + uint64 _sourceChain, + address _sourceAddress, + bytes memory _payload, + bytes memory _extraArgs + ) internal override { + /// post cross chain transaction received logic goes here + } + + function _sendMessagePayToken( + uint64 _destinationChain, + address _recipient, + bytes calldata _data, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes memory _extraArgs + ) internal { + Client.EVM2AnyMessage memory evm2AnyMessage = + _buildCCIPMessage(_recipient, _data, _token, _amount, _feeTokenAddress, _extraArgs); + IRouterClient router = IRouterClient(_chainlinkCrossChainStorage().router); + uint256 fees = router.getFee(_destinationChain, evm2AnyMessage); + IERC20 linkToken = IERC20(_chainlinkCrossChainStorage().linkToken); + + if (fees > linkToken.balanceOf(address(this))) { + revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees); + } + + IERC20(linkToken).approve(address(router), fees); + IERC20(_token).approve(address(router), _amount); + router.ccipSend(_destinationChain, evm2AnyMessage); + } + + function _sendMessagePayNative( + uint64 _destinationChain, + address _recipient, + bytes calldata _data, + address _token, + uint256 _amount, + bytes memory _extraArgs + ) internal { + Client.EVM2AnyMessage memory evm2AnyMessage = + _buildCCIPMessage(_recipient, _data, _token, _amount, address(0), _extraArgs); + IRouterClient router = IRouterClient(_chainlinkCrossChainStorage().router); + uint256 fees = router.getFee(_destinationChain, evm2AnyMessage); + + if (fees > address(this).balance) { + revert NotEnoughBalance(address(this).balance, fees); + } + + IERC20(_token).approve(address(router), _amount); + router.ccipSend{value: fees}(_destinationChain, evm2AnyMessage); + } + + function _buildCCIPMessage( + address _recipient, + bytes calldata _data, + address _token, + uint256 _amount, + address _feeTokenAddress, + bytes memory _extraArgs + ) private pure returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: _token, amount: _amount}); + + return Client.EVM2AnyMessage({ + receiver: abi.encode(_recipient), + data: _data, + tokenAmounts: tokenAmounts, + extraArgs: _extraArgs, + feeToken: _feeTokenAddress + }); + } + + function _ccipReceive(Client.Any2EVMMessage memory message) internal override { + address sender = abi.decode(message.sender, (address)); + bytes memory payload = ""; + onCrossChainTransactionReceived(message.sourceChainSelector, sender, message.data, payload); + } + + function _chainlinkCrossChainStorage() internal pure returns (ChainlinkCrossChainStorage.Data storage) { + return ChainlinkCrossChainStorage.data(); + } + +}