Skip to content

Commit daf9fbc

Browse files
aelmanaadwightjlthedriftofwords
authored
manual execution guide (#1669)
* manual execution * use avalanche fuji as source chain * clarifications * clarifications * clarifications * clarifications * clarifications * clarifications * clarifications * clarifications * clarifications * clarifications * clarifications * clarifications * service limits * clarifications * Apply suggestions from code review Co-authored-by: Dwight Lyle <[email protected]> Co-authored-by: Crystal Gomes <[email protected]> * add contact banner * Apply suggestions from code review --------- Co-authored-by: Dwight Lyle <[email protected]> Co-authored-by: Crystal Gomes <[email protected]>
1 parent e416fb5 commit daf9fbc

27 files changed

+669
-31
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"IERC",
3737
"Linea",
3838
"Mainnets",
39+
"Merkle",
3940
"MOONRIVER",
4041
"MOVR",
4142
"Numberish",
@@ -55,6 +56,7 @@
5556
"typechain",
5657
"WAVAX",
5758
"WBNB",
59+
"whatsnext",
5860
"WMATIC",
5961
"XDAI"
6062
]
18.8 KB
Loading
364 KB
Loading
Loading
Loading
Loading
Loading
Loading
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.19;
3+
4+
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
5+
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
6+
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
7+
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
8+
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/contracts/token/ERC20/IERC20.sol";
9+
10+
/**
11+
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
12+
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
13+
* DO NOT USE THIS CODE IN PRODUCTION.
14+
*/
15+
16+
/// @title - A simple messenger contract for transferring/receiving tokens and data across chains.
17+
contract ProgrammableTokenTransfersLowGasLimit is CCIPReceiver, OwnerIsCreator {
18+
// Custom errors to provide more descriptive revert messages.
19+
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance to cover the fees.
20+
error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
21+
error DestinationChainNotAllowed(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
22+
error SourceChainNotAllowed(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
23+
error SenderNotAllowed(address sender); // Used when the sender has not been allowlisted by the contract owner.
24+
25+
// Event emitted when a message is sent to another chain.
26+
event MessageSent(
27+
bytes32 indexed messageId, // The unique ID of the CCIP message.
28+
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
29+
address receiver, // The address of the receiver on the destination chain.
30+
string text, // The text being sent.
31+
address token, // The token address that was transferred.
32+
uint256 tokenAmount, // The token amount that was transferred.
33+
address feeToken, // the token address used to pay CCIP fees.
34+
uint256 fees // The fees paid for sending the message.
35+
);
36+
37+
// Event emitted when a message is received from another chain.
38+
event MessageReceived(
39+
bytes32 indexed messageId, // The unique ID of the CCIP message.
40+
uint64 indexed sourceChainSelector, // The chain selector of the source chain.
41+
address sender, // The address of the sender from the source chain.
42+
string text, // The text that was received.
43+
address token, // The token address that was transferred.
44+
uint256 tokenAmount // The token amount that was transferred.
45+
);
46+
47+
bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
48+
address private s_lastReceivedTokenAddress; // Store the last received token address.
49+
uint256 private s_lastReceivedTokenAmount; // Store the last received amount.
50+
string private s_lastReceivedText; // Store the last received text.
51+
52+
// Mapping to keep track of allowlisted destination chains.
53+
mapping(uint64 => bool) public allowlistedDestinationChains;
54+
55+
// Mapping to keep track of allowlisted source chains.
56+
mapping(uint64 => bool) public allowlistedSourceChains;
57+
58+
// Mapping to keep track of allowlisted senders.
59+
mapping(address => bool) public allowlistedSenders;
60+
61+
IERC20 private s_linkToken;
62+
63+
/// @notice Constructor initializes the contract with the router address.
64+
/// @param _router The address of the router contract.
65+
/// @param _link The address of the link contract.
66+
constructor(address _router, address _link) CCIPReceiver(_router) {
67+
s_linkToken = IERC20(_link);
68+
}
69+
70+
/// @dev Modifier that checks if the chain with the given destinationChainSelector is allowlisted.
71+
/// @param _destinationChainSelector The selector of the destination chain.
72+
modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {
73+
if (!allowlistedDestinationChains[_destinationChainSelector])
74+
revert DestinationChainNotAllowed(_destinationChainSelector);
75+
_;
76+
}
77+
78+
/// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted.
79+
/// @param _sourceChainSelector The selector of the destination chain.
80+
/// @param _sender The address of the sender.
81+
modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {
82+
if (!allowlistedSourceChains[_sourceChainSelector])
83+
revert SourceChainNotAllowed(_sourceChainSelector);
84+
if (!allowlistedSenders[_sender]) revert SenderNotAllowed(_sender);
85+
_;
86+
}
87+
88+
/// @dev Updates the allowlist status of a destination chain for transactions.
89+
/// @notice This function can only be called by the owner.
90+
/// @param _destinationChainSelector The selector of the destination chain to be updated.
91+
/// @param allowed The allowlist status to be set for the destination chain.
92+
function allowlistDestinationChain(
93+
uint64 _destinationChainSelector,
94+
bool allowed
95+
) external onlyOwner {
96+
allowlistedDestinationChains[_destinationChainSelector] = allowed;
97+
}
98+
99+
/// @dev Updates the allowlist status of a source chain
100+
/// @notice This function can only be called by the owner.
101+
/// @param _sourceChainSelector The selector of the source chain to be updated.
102+
/// @param allowed The allowlist status to be set for the source chain.
103+
function allowlistSourceChain(
104+
uint64 _sourceChainSelector,
105+
bool allowed
106+
) external onlyOwner {
107+
allowlistedSourceChains[_sourceChainSelector] = allowed;
108+
}
109+
110+
/// @dev Updates the allowlist status of a sender for transactions.
111+
/// @notice This function can only be called by the owner.
112+
/// @param _sender The address of the sender to be updated.
113+
/// @param allowed The allowlist status to be set for the sender.
114+
function allowlistSender(address _sender, bool allowed) external onlyOwner {
115+
allowlistedSenders[_sender] = allowed;
116+
}
117+
118+
/// @notice Sends data and transfer tokens to receiver on the destination chain.
119+
/// @notice Pay for fees in LINK.
120+
/// @notice the gasLimit is set to 20_000 on purpose to force the execution to fail on the destination chain
121+
/// @dev Assumes your contract has sufficient LINK to pay for CCIP fees.
122+
/// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
123+
/// @param _receiver The address of the recipient on the destination blockchain.
124+
/// @param _text The string data to be sent.
125+
/// @param _token token address.
126+
/// @param _amount token amount.
127+
/// @return messageId The ID of the CCIP message that was sent.
128+
function sendMessagePayLINK(
129+
uint64 _destinationChainSelector,
130+
address _receiver,
131+
string calldata _text,
132+
address _token,
133+
uint256 _amount
134+
)
135+
external
136+
onlyOwner
137+
onlyAllowlistedDestinationChain(_destinationChainSelector)
138+
returns (bytes32 messageId)
139+
{
140+
// Set the token amounts
141+
Client.EVMTokenAmount[]
142+
memory tokenAmounts = new Client.EVMTokenAmount[](1);
143+
tokenAmounts[0] = Client.EVMTokenAmount({
144+
token: _token,
145+
amount: _amount
146+
});
147+
148+
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
149+
// address(linkToken) means fees are paid in LINK
150+
151+
Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
152+
receiver: abi.encode(_receiver), // ABI-encoded receiver address
153+
data: abi.encode(_text), // ABI-encoded string
154+
tokenAmounts: tokenAmounts, // The amount and type of token being transferred
155+
extraArgs: Client._argsToBytes(
156+
// gasLimit set to 20_000 on purpose to force the execution to fail on the destination chain
157+
Client.EVMExtraArgsV1({gasLimit: 20_000})
158+
),
159+
// Set the feeToken to a LINK token address
160+
feeToken: address(s_linkToken)
161+
});
162+
163+
// Initialize a router client instance to interact with cross-chain router
164+
IRouterClient router = IRouterClient(this.getRouter());
165+
166+
// Get the fee required to send the CCIP message
167+
uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
168+
169+
if (fees > s_linkToken.balanceOf(address(this)))
170+
revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
171+
172+
// approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
173+
s_linkToken.approve(address(router), fees);
174+
175+
// approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token
176+
IERC20(_token).approve(address(router), _amount);
177+
178+
// Send the message through the router and store the returned message ID
179+
messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
180+
181+
// Emit an event with message details
182+
emit MessageSent(
183+
messageId,
184+
_destinationChainSelector,
185+
_receiver,
186+
_text,
187+
_token,
188+
_amount,
189+
address(s_linkToken),
190+
fees
191+
);
192+
193+
// Return the message ID
194+
return messageId;
195+
}
196+
197+
/**
198+
* @notice Returns the details of the last CCIP received message.
199+
* @dev This function retrieves the ID, text, token address, and token amount of the last received CCIP message.
200+
* @return messageId The ID of the last received CCIP message.
201+
* @return text The text of the last received CCIP message.
202+
* @return tokenAddress The address of the token in the last CCIP received message.
203+
* @return tokenAmount The amount of the token in the last CCIP received message.
204+
*/
205+
function getLastReceivedMessageDetails()
206+
public
207+
view
208+
returns (
209+
bytes32 messageId,
210+
string memory text,
211+
address tokenAddress,
212+
uint256 tokenAmount
213+
)
214+
{
215+
return (
216+
s_lastReceivedMessageId,
217+
s_lastReceivedText,
218+
s_lastReceivedTokenAddress,
219+
s_lastReceivedTokenAmount
220+
);
221+
}
222+
223+
/// handle a received message
224+
function _ccipReceive(
225+
Client.Any2EVMMessage memory any2EvmMessage
226+
)
227+
internal
228+
override
229+
onlyAllowlisted(
230+
any2EvmMessage.sourceChainSelector,
231+
abi.decode(any2EvmMessage.sender, (address))
232+
) // Make sure source chain and sender are allowlisted
233+
{
234+
s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
235+
s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text
236+
// Expect one token to be transferred at once, but you can transfer several tokens.
237+
s_lastReceivedTokenAddress = any2EvmMessage.destTokenAmounts[0].token;
238+
s_lastReceivedTokenAmount = any2EvmMessage.destTokenAmounts[0].amount;
239+
240+
emit MessageReceived(
241+
any2EvmMessage.messageId,
242+
any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector)
243+
abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address,
244+
abi.decode(any2EvmMessage.data, (string)),
245+
any2EvmMessage.destTokenAmounts[0].token,
246+
any2EvmMessage.destTokenAmounts[0].amount
247+
);
248+
}
249+
250+
/// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
251+
/// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
252+
/// @param _beneficiary The address to which the tokens will be sent.
253+
/// @param _token The contract address of the ERC20 token to be withdrawn.
254+
function withdrawToken(
255+
address _beneficiary,
256+
address _token
257+
) public onlyOwner {
258+
// Retrieve the balance of this contract
259+
uint256 amount = IERC20(_token).balanceOf(address(this));
260+
261+
// Revert if there is nothing to withdraw
262+
if (amount == 0) revert NothingToWithdraw();
263+
264+
IERC20(_token).transfer(_beneficiary, amount);
265+
}
266+
}

src/config/sidebar.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,10 @@ export const SIDEBAR: Partial<Record<Sections, SectionEntry[]>> = {
885885
title: "Send Arbitrary Data",
886886
url: "ccip/tutorials/send-arbitrary-data",
887887
},
888+
{
889+
title: "Manual Execution",
890+
url: "ccip/tutorials/manual-execution",
891+
},
888892
{
889893
title: "Acquire Test Tokens",
890894
url: "ccip/test-tokens",
@@ -895,13 +899,17 @@ export const SIDEBAR: Partial<Record<Sections, SectionEntry[]>> = {
895899
section: "Concepts",
896900
contents: [
897901
{
898-
title: "Concept Overview",
902+
title: "Conceptual Overview",
899903
url: "ccip/concepts",
900904
},
901905
{
902906
title: "Architecture",
903907
url: "ccip/architecture",
904908
},
909+
{
910+
title: "Manual execution",
911+
url: "ccip/concepts/manual-execution",
912+
},
905913
{
906914
title: "Best Practices",
907915
url: "ccip/best-practices",

src/content/ccip/architecture.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ date: Last Modified
44
title: "CCIP Architecture"
55
whatsnext:
66
{
7+
"CCIP manual execution": "/ccip/concepts/manual-execution",
78
"Learn CCIP best practices": "/ccip/best-practices",
89
"Find the list of supported networks, lanes, and rate limits on the CCIP Supported Networks page": "/ccip/supported-networks",
910
}
@@ -169,4 +170,4 @@ Chainlink CCIP has been purposely designed to take a security-first approach to
169170

170171
If the fee paid on the source chain is within an acceptable range of the execution cost on the destination chain, the message will be transmitted as soon as possible after it is blessed by the Risk Management Network. If the cost of execution increases between request and execution time, CCIP incrementally increases the gas price to attempt to reach eventual execution, but this might introduce additional latency.
171172

172-
Execution delays in excess of one hour should be rare as a result of _Smart Execution_. The `permissionlessExecutionThresholdSeconds` parameter represents the waiting time before manual execution is enabled. If the DON fails to execute within the duration of `permissionlessExecutionThresholdSeconds` due to extreme network conditions, you can manually execute the message through the [CCIP Explorer](https://ccip.chain.link/).
173+
Execution delays in excess of one hour should be rare as a result of _Smart Execution_. The Smart Execution time window parameter represents the waiting time before manual execution is enabled. If the DON fails to execute within the duration of Smart Execution time window due to extreme network conditions, you can manually execute the message through the [CCIP Explorer](https://ccip.chain.link/). Read the [manual execution](/ccip/concepts/manual-execution) conceptual guide to learn more.

src/content/ccip/best-practices.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ The purpose of `extraArgs` is to allow compatibility with future CCIP upgrades.
5353

5454
If `extraArgs` are left empty, a default of _200000_ `gasLimit` will be set.
5555

56+
## Decoupling CCIP Message Reception and Business Logic
57+
58+
As a best practice, separate the reception of CCIP messages from the core business logic of the contract. Implement 'escape hatches' or fallback mechanisms to gracefully manage situations where the business logic encounters issues. To explore this concept further, refer to the [Defensive Example](/ccip/tutorials/programmable-token-transfers-defensive) guide.
59+
5660
## Evaluate the security and reliability of the networks that you use
5761

5862
Although CCIP has been thoroughly reviewed and audited, inherent risks might still exist based on your use case, the blockchain networks where you deploy your contracts, and the network conditions on those blockchains.
File renamed without changes.

0 commit comments

Comments
 (0)