From 434f4ae941b36d9841d6e39d5d7e534de42d3b43 Mon Sep 17 00:00:00 2001 From: koteld Date: Mon, 4 Mar 2024 10:30:20 +0100 Subject: [PATCH 1/6] introduce interface and library to interact with CCIP Router --- src/interfaces/ccip/CCIPRouterInterface.sol | 17 +++++++++++++++++ src/libraries/CCIPClient.sol | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 src/interfaces/ccip/CCIPRouterInterface.sol create mode 100644 src/libraries/CCIPClient.sol diff --git a/src/interfaces/ccip/CCIPRouterInterface.sol b/src/interfaces/ccip/CCIPRouterInterface.sol new file mode 100644 index 0000000..de50028 --- /dev/null +++ b/src/interfaces/ccip/CCIPRouterInterface.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2 <0.9.0; + +import "../shared/TypeAndVersionInterface.sol"; +import "src/libraries/CCIPClient.sol"; + +interface CCIPRouterInterface is TypeAndVersionInterface { + function getFee( + uint64 destinationChainSelector, + Client.EVM2AnyMessage memory message + ) external view returns (uint256 fee); + function getSupportedTokens(uint64 chainSelector) external view returns (address[] memory); + function isChainSupported(uint64 chainSelector) external view returns (bool); + function getOnRamp(uint64 destChainSelector) external view returns (address); + function isOffRamp(uint64 sourceChainSelector, address offRamp) external view returns (bool); + function getWrappedNative() external view returns (address); +} diff --git a/src/libraries/CCIPClient.sol b/src/libraries/CCIPClient.sol new file mode 100644 index 0000000..f26e6c6 --- /dev/null +++ b/src/libraries/CCIPClient.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2 <0.9.0; + +/// @title Library of common constants and enums for the CCIP Router contract +library Client { + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct EVMTokenAmount { + address token; // token address on the local chain. + uint256 amount; // Amount of tokens. + } + + // If extraArgs is empty bytes, the default is 200k gas limit. + struct EVM2AnyMessage { + bytes receiver; // abi.encode(receiver address) for dest EVM chains + bytes data; // Data payload + EVMTokenAmount[] tokenAmounts; // Token transfers + address feeToken; // Address of feeToken. address(0) means you will send msg.value. + bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1) + } +} From bfa4b68a841646053896ac9eced71b60097e6c73 Mon Sep 17 00:00:00 2001 From: koteld Date: Mon, 4 Mar 2024 10:31:26 +0100 Subject: [PATCH 2/6] add Solidity scripts and CLI wrappers to interact with CCIP Router --- script/ccip/CCIPRouter.CLI.s.sol | 56 ++++++++++++++++++++++++++++++++ script/ccip/CCIPRouter.s.sol | 49 ++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 script/ccip/CCIPRouter.CLI.s.sol create mode 100644 script/ccip/CCIPRouter.s.sol diff --git a/script/ccip/CCIPRouter.CLI.s.sol b/script/ccip/CCIPRouter.CLI.s.sol new file mode 100644 index 0000000..0252533 --- /dev/null +++ b/script/ccip/CCIPRouter.CLI.s.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2 <0.9.0; + +import "forge-std/Script.sol"; + +import "./CCIPRouter.s.sol"; +import "../helpers/BaseScript.s.sol"; + +contract CCIPRouterCLIScript is BaseScript { + function getFee( + address ccipRouterAddress, + uint64 destinationChainSelector, + Client.EVM2AnyMessage memory message + ) external view returns (uint256 fee) { + CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); + return ccipRouterScript.getFee(destinationChainSelector, message); + } + + function getSupportedTokens( + address ccipRouterAddress, + uint64 chainSelector + ) external view returns (address[] memory) { + CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); + return ccipRouterScript.getSupportedTokens(chainSelector); + } + + function isChainSupported( + address ccipRouterAddress, + uint64 chainSelector + ) public view returns (bool) { + CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); + return ccipRouterScript.isChainSupported(chainSelector); + } + + function getOnRamp( + address ccipRouterAddress, + uint64 destChainSelector + ) external view returns (address) { + CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); + return ccipRouterScript.getOnRamp(destChainSelector); + } + + function isOffRamp( + address ccipRouterAddress, + uint64 sourceChainSelector, + address offRamp + ) public view returns (bool) { + CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); + return ccipRouterScript.isOffRamp(sourceChainSelector, offRamp); + } + + function getWrappedNative(address ccipRouterAddress) external view returns (address) { + CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); + return ccipRouterScript.getWrappedNative(); + } +} diff --git a/script/ccip/CCIPRouter.s.sol b/script/ccip/CCIPRouter.s.sol new file mode 100644 index 0000000..3aa1b46 --- /dev/null +++ b/script/ccip/CCIPRouter.s.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2 <0.9.0; + +import "forge-std/Script.sol"; + +import "../helpers/BaseScript.s.sol"; +import "../helpers/TypeAndVersion.s.sol"; +import "src/interfaces/ccip/CCIPRouterInterface.sol"; + +contract CCIPRouterScript is BaseScript, TypeAndVersionScript { + address public ccipRouterAddress; + + constructor (address _ccipRouterAddress) { + ccipRouterAddress = _ccipRouterAddress; + } + + function getFee( + uint64 destinationChainSelector, + Client.EVM2AnyMessage memory message + ) external view returns (uint256 fee) { + CCIPRouterInterface ccipRouter = CCIPRouterInterface(ccipRouterAddress); + return ccipRouter.getFee(destinationChainSelector, message); + } + + function getSupportedTokens(uint64 chainSelector) external view returns (address[] memory) { + CCIPRouterInterface ccipRouter = CCIPRouterInterface(ccipRouterAddress); + return ccipRouter.getSupportedTokens(chainSelector); + } + + function isChainSupported(uint64 chainSelector) public view returns (bool) { + CCIPRouterInterface ccipRouter = CCIPRouterInterface(ccipRouterAddress); + return ccipRouter.isChainSupported(chainSelector); + } + + function getOnRamp(uint64 destChainSelector) external view returns (address) { + CCIPRouterInterface ccipRouter = CCIPRouterInterface(ccipRouterAddress); + return ccipRouter.getOnRamp(destChainSelector); + } + + function isOffRamp(uint64 sourceChainSelector, address offRamp) public view returns (bool) { + CCIPRouterInterface ccipRouter = CCIPRouterInterface(ccipRouterAddress); + return ccipRouter.isOffRamp(sourceChainSelector, offRamp); + } + + function getWrappedNative() external view returns (address) { + CCIPRouterInterface ccipRouter = CCIPRouterInterface(ccipRouterAddress); + return ccipRouter.getWrappedNative(); + } +} From d8c340ff414115cd2989dc751fa30c34d48eacab Mon Sep 17 00:00:00 2001 From: koteld Date: Mon, 4 Mar 2024 11:04:01 +0100 Subject: [PATCH 3/6] fix functions modifiers, visibility and mutability --- script/automation/Automation.CLI.s.sol | 2 +- script/ccip/CCIPRouter.CLI.s.sol | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/script/automation/Automation.CLI.s.sol b/script/automation/Automation.CLI.s.sol index 6b2ec95..8630a4e 100644 --- a/script/automation/Automation.CLI.s.sol +++ b/script/automation/Automation.CLI.s.sol @@ -16,7 +16,7 @@ contract AutomationCLIScript is BaseScript { address upkeepAddress, uint32 gasLimit, bytes calldata checkData - ) nestedScriptContext public returns (bytes32 requestHash) { + ) nestedScriptContext external returns (bytes32 requestHash) { AutomationScript automationScript = new AutomationScript(keeperRegistryAddress); return automationScript.registerUpkeep( amountInJuels, diff --git a/script/ccip/CCIPRouter.CLI.s.sol b/script/ccip/CCIPRouter.CLI.s.sol index 0252533..3683559 100644 --- a/script/ccip/CCIPRouter.CLI.s.sol +++ b/script/ccip/CCIPRouter.CLI.s.sol @@ -11,7 +11,7 @@ contract CCIPRouterCLIScript is BaseScript { address ccipRouterAddress, uint64 destinationChainSelector, Client.EVM2AnyMessage memory message - ) external view returns (uint256 fee) { + ) external returns (uint256 fee) { CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); return ccipRouterScript.getFee(destinationChainSelector, message); } @@ -19,7 +19,7 @@ contract CCIPRouterCLIScript is BaseScript { function getSupportedTokens( address ccipRouterAddress, uint64 chainSelector - ) external view returns (address[] memory) { + ) external returns (address[] memory) { CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); return ccipRouterScript.getSupportedTokens(chainSelector); } @@ -27,7 +27,7 @@ contract CCIPRouterCLIScript is BaseScript { function isChainSupported( address ccipRouterAddress, uint64 chainSelector - ) public view returns (bool) { + ) external returns (bool) { CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); return ccipRouterScript.isChainSupported(chainSelector); } @@ -35,7 +35,7 @@ contract CCIPRouterCLIScript is BaseScript { function getOnRamp( address ccipRouterAddress, uint64 destChainSelector - ) external view returns (address) { + ) external returns (address) { CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); return ccipRouterScript.getOnRamp(destChainSelector); } @@ -44,12 +44,12 @@ contract CCIPRouterCLIScript is BaseScript { address ccipRouterAddress, uint64 sourceChainSelector, address offRamp - ) public view returns (bool) { + ) external returns (bool) { CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); return ccipRouterScript.isOffRamp(sourceChainSelector, offRamp); } - function getWrappedNative(address ccipRouterAddress) external view returns (address) { + function getWrappedNative(address ccipRouterAddress) external returns (address) { CCIPRouterScript ccipRouterScript = new CCIPRouterScript(ccipRouterAddress); return ccipRouterScript.getWrappedNative(); } From 2ec2dd99c7f41c033b8e12a155fe139374b73968 Mon Sep 17 00:00:00 2001 From: koteld Date: Wed, 13 Mar 2024 17:51:19 +0100 Subject: [PATCH 4/6] add interfaces to interact with Receiver --- src/interfaces/ccip/IAny2EVMMessageReceiver.sol | 15 +++++++++++++++ src/libraries/CCIPClient.sol | 8 ++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/interfaces/ccip/IAny2EVMMessageReceiver.sol diff --git a/src/interfaces/ccip/IAny2EVMMessageReceiver.sol b/src/interfaces/ccip/IAny2EVMMessageReceiver.sol new file mode 100644 index 0000000..dde6df8 --- /dev/null +++ b/src/interfaces/ccip/IAny2EVMMessageReceiver.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2 <0.9.0; + +import "src/libraries/CCIPClient.sol"; + +/// @notice Application contracts that intend to receive messages from +/// the router should implement this interface. +interface IAny2EVMMessageReceiver { + /// @notice Called by the Router to deliver a message. + /// If this reverts, any token transfers also revert. The message + /// will move to a FAILED state and become available for manual execution. + /// @param message CCIP Message + /// @dev Note ensure you check the msg.sender is the OffRampRouter + function ccipReceive(Client.Any2EVMMessage calldata message) external; +} diff --git a/src/libraries/CCIPClient.sol b/src/libraries/CCIPClient.sol index f26e6c6..a3d81c6 100644 --- a/src/libraries/CCIPClient.sol +++ b/src/libraries/CCIPClient.sol @@ -17,4 +17,12 @@ library Client { address feeToken; // Address of feeToken. address(0) means you will send msg.value. bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1) } + + struct Any2EVMMessage { + bytes32 messageId; // MessageId corresponding to ccipSend on source. + uint64 sourceChainSelector; // Source chain selector. + bytes sender; // abi.decode(sender) if coming from an EVM chain. + bytes data; // payload sent in original message. + EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation. + } } From 9998d40891fe936953529f87fd2e82f3296830fb Mon Sep 17 00:00:00 2001 From: koteld Date: Wed, 13 Mar 2024 17:52:53 +0100 Subject: [PATCH 5/6] update CCIP service module --- script/ccip/CCIPReceiver.CLI.s.sol | 27 +++++++++++++++++++++++++++ script/ccip/CCIPReceiver.s.sol | 23 +++++++++++++++++++++++ script/ccip/CCIPRouter.CLI.s.sol | 1 + script/ccip/CCIPRouter.s.sol | 1 + 4 files changed, 52 insertions(+) create mode 100644 script/ccip/CCIPReceiver.CLI.s.sol create mode 100644 script/ccip/CCIPReceiver.s.sol diff --git a/script/ccip/CCIPReceiver.CLI.s.sol b/script/ccip/CCIPReceiver.CLI.s.sol new file mode 100644 index 0000000..86b11c4 --- /dev/null +++ b/script/ccip/CCIPReceiver.CLI.s.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2 <0.9.0; + +import "forge-std/Script.sol"; + +import "./CCIPReceiver.s.sol"; +import "../helpers/BaseScript.s.sol"; + +contract CCIPReceiverCLIScript is BaseScript { + function ccipReceive( + address ccipReceiverAddress, + uint64 sourceChainSelector, + address sender, + bytes memory data + ) external { + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](0); + CCIPReceiverScript ccipReceiverScript = new CCIPReceiverScript(ccipReceiverAddress); + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: bytes32(0), + sourceChainSelector: sourceChainSelector, + sender: abi.encode(sender), + data: data, + destTokenAmounts: destTokenAmounts + }); + ccipReceiverScript.ccipReceive(message); + } +} diff --git a/script/ccip/CCIPReceiver.s.sol b/script/ccip/CCIPReceiver.s.sol new file mode 100644 index 0000000..71c1575 --- /dev/null +++ b/script/ccip/CCIPReceiver.s.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2 <0.9.0; + +import "forge-std/Script.sol"; + +import "../helpers/BaseScript.s.sol"; +import "src/interfaces/ccip/IAny2EVMMessageReceiver.sol"; +import "src/libraries/CCIPClient.sol"; + +contract CCIPReceiverScript is BaseScript { + address public ccipReceiverAddress; + + constructor (address _ccipReceiverAddress) { + ccipReceiverAddress = _ccipReceiverAddress; + } + + function ccipReceive( + Client.Any2EVMMessage memory message + ) external { + IAny2EVMMessageReceiver ccipReceiver = IAny2EVMMessageReceiver(ccipReceiverAddress); + ccipReceiver.ccipReceive(message); + } +} diff --git a/script/ccip/CCIPRouter.CLI.s.sol b/script/ccip/CCIPRouter.CLI.s.sol index 3683559..8675671 100644 --- a/script/ccip/CCIPRouter.CLI.s.sol +++ b/script/ccip/CCIPRouter.CLI.s.sol @@ -5,6 +5,7 @@ import "forge-std/Script.sol"; import "./CCIPRouter.s.sol"; import "../helpers/BaseScript.s.sol"; +import "src/libraries/CCIPClient.sol"; contract CCIPRouterCLIScript is BaseScript { function getFee( diff --git a/script/ccip/CCIPRouter.s.sol b/script/ccip/CCIPRouter.s.sol index 3aa1b46..7021b79 100644 --- a/script/ccip/CCIPRouter.s.sol +++ b/script/ccip/CCIPRouter.s.sol @@ -6,6 +6,7 @@ import "forge-std/Script.sol"; import "../helpers/BaseScript.s.sol"; import "../helpers/TypeAndVersion.s.sol"; import "src/interfaces/ccip/CCIPRouterInterface.sol"; +import "src/libraries/CCIPClient.sol"; contract CCIPRouterScript is BaseScript, TypeAndVersionScript { address public ccipRouterAddress; From 2dced0d9f70c3ab4b04c4d27e1dad25814c128bc Mon Sep 17 00:00:00 2001 From: koteld Date: Wed, 13 Mar 2024 17:53:46 +0100 Subject: [PATCH 6/6] add script to estimate gas of CCIPReceive on a destination chain --- makefile-external | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/makefile-external b/makefile-external index 5b47c7c..81604fd 100644 --- a/makefile-external +++ b/makefile-external @@ -54,3 +54,12 @@ fct-vrf-cancel-subscription: fct-check-rpc-url $(call check_set_parameter,RECEIVING_ADDRESS,receivingAddress) \ forge script ${FCT_PLUGIN_PATH}/script/vrf/VRF.CLI.s.sol --sig "cancelSubscription(address,uint64,address)" $$functionsRouterAddress $$subId $$receivingAddress --rpc-url ${RPC_URL} --broadcast --private-key ${PRIVATE_KEY} --out ${FCT_PLUGIN_PATH}/out +# CCIP +.PHONY: fcc-ccip-estimate-receive +fcc-ccip-estimate-receive: + $(call check_set_parameter,DESTINATION_RPC_URL,destinationRpcUrl) \ + $(call check_set_parameter,CCIP_ROUTER_ADDRESS,ccipReceiverAddress) \ + $(call check_set_parameter,SOURCE_CHAIN_SELECTOR,sourceChainSelector) \ + $(call check_set_parameter,CCIP_MESSAGE_SENDER,ccipMessageSender) \ + $(call check_set_parameter,CCIP_MESSAGE_DATA,ccipMessageData) \ + forge script ${FCT_PLUGIN_PATH}/script/ccip/CCIPReceiver.CLI.s.sol --sig "ccipReceive(address,uint64,address,bytes)" $$ccipReceiverAddress $$sourceChainSelector $$ccipMessageSender $$ccipMessageData --rpc-url $$destinationRpcUrl --out ${FCT_PLUGIN_PATH}/out