Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CCIP Defensive example - add pagination #1749

Merged
merged 3 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"preconfigured",
"quickstarts",
"Sepolia",
"struct",
"tbody",
"thead",
"THENA",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions public/samples/CCIP/Messenger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ contract Messenger is CCIPReceiver, OwnerIsCreator {
error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
error SourceChainNotAllowlisted(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
error SenderNotAllowlisted(address sender); // Used when the sender has not been allowlisted by the contract owner.
error InvalidReceiverAddress(); // Used when the receiver address is 0.

// Event emitted when a message is sent to another chain.
event MessageSent(
Expand Down Expand Up @@ -80,6 +81,13 @@ contract Messenger is CCIPReceiver, OwnerIsCreator {
_;
}

/// @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.
function allowlistDestinationChain(
uint64 _destinationChainSelector,
Expand Down Expand Up @@ -116,6 +124,7 @@ contract Messenger is CCIPReceiver, OwnerIsCreator {
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Expand Down Expand Up @@ -169,6 +178,7 @@ contract Messenger is CCIPReceiver, OwnerIsCreator {
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Expand Down
63 changes: 45 additions & 18 deletions public/samples/CCIP/ProgrammableDefensiveTokenTransfers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
error SourceChainNotAllowed(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
error SenderNotAllowed(address sender); // Used when the sender has not been allowlisted by the contract owner.
error InvalidReceiverAddress(); // Used when the receiver address is 0.
error OnlySelf(); // Used when a function is called outside of the contract itself.
error ErrorCase(); // Used when simulating a revert during message processing.
error MessageNotFailed(bytes32 messageId);
Expand All @@ -37,7 +38,12 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
// RESOLVED is first so that the default value is resolved.
RESOLVED,
// Could have any number of error codes here.
BASIC
FAILED
}

struct FailedMessage {
bytes32 messageId;
ErrorCode errorCode;
}

// Event emitted when a message is sent to another chain.
Expand Down Expand Up @@ -116,6 +122,13 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
_;
}

/// @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 Modifier to allow only the contract itself to execute a function.
/// Throws an exception if called by any account other than the contract itself.
modifier onlySelf() {
Expand Down Expand Up @@ -172,6 +185,7 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Expand Down Expand Up @@ -237,6 +251,7 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Expand Down Expand Up @@ -310,22 +325,34 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
}

/**
* @notice Retrieves the IDs of failed messages from the `s_failedMessages` map.
* @dev Iterates over the `s_failedMessages` map, collecting all keys.
* @return ids An array of bytes32 containing the IDs of failed messages from the `s_failedMessages` map.
* @notice Retrieves a paginated list of failed messages.
* @dev This function returns a subset of failed messages defined by `offset` and `limit` parameters. It ensures that the pagination parameters are within the bounds of the available data set.
* @param offset The index of the first failed message to return, enabling pagination by skipping a specified number of messages from the start of the dataset.
* @param limit The maximum number of failed messages to return, restricting the size of the returned array.
* @return failedMessages An array of `FailedMessage` struct, each containing a `messageId` and an `errorCode` (RESOLVED or FAILED), representing the requested subset of failed messages. The length of the returned array is determined by the `limit` and the total number of failed messages.
*/
function getFailedMessagesIds()
external
view
returns (bytes32[] memory ids)
{
function getFailedMessages(
uint256 offset,
uint256 limit
) external view returns (FailedMessage[] memory) {
uint256 length = s_failedMessages.length();
bytes32[] memory allKeys = new bytes32[](length);
for (uint256 i = 0; i < length; i++) {
(bytes32 key, ) = s_failedMessages.at(i);
allKeys[i] = key;

// Calculate the actual number of items to return (can't exceed total length or requested limit)
uint256 returnLength = (offset + limit > length)
? length - offset
: limit;
FailedMessage[] memory failedMessages = new FailedMessage[](
returnLength
);

// Adjust loop to respect pagination (start at offset, end at offset + limit or total length)
for (uint256 i = 0; i < returnLength; i++) {
(bytes32 messageId, uint256 errorCode) = s_failedMessages.at(
offset + i
);
failedMessages[i] = FailedMessage(messageId, ErrorCode(errorCode));
}
return allKeys;
return failedMessages;
}

/// @notice The entrypoint for the CCIP router to call. This function should
Expand All @@ -351,7 +378,7 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
// handled differently.
s_failedMessages.set(
any2EvmMessage.messageId,
uint256(ErrorCode.BASIC)
uint256(ErrorCode.FAILED)
);
s_messageContents[any2EvmMessage.messageId] = any2EvmMessage;
// Don't revert so CCIP doesn't revert. Emit event instead.
Expand Down Expand Up @@ -392,7 +419,7 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
address tokenReceiver
) external onlyOwner {
// Check if the message has failed; if not, revert the transaction.
if (s_failedMessages.get(messageId) != uint256(ErrorCode.BASIC))
if (s_failedMessages.get(messageId) != uint256(ErrorCode.FAILED))
revert MessageNotFailed(messageId);

// Set the error code to RESOLVED to disallow reentry and multiple retries of the same failed message.
Expand Down Expand Up @@ -467,7 +494,7 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
tokenAmounts: tokenAmounts, // The amount and type of token being transferred
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit
Client.EVMExtraArgsV1({gasLimit: 2_000_000})
Client.EVMExtraArgsV1({gasLimit: 400_000})
),
// Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
feeToken: _feeTokenAddress
Expand Down Expand Up @@ -512,6 +539,6 @@ contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
// Revert if there is nothing to withdraw
if (amount == 0) revert NothingToWithdraw();

IERC20(_token).transfer(_beneficiary, amount);
IERC20(_token).safeTransfer(_beneficiary, amount);
}
}
10 changes: 10 additions & 0 deletions public/samples/CCIP/ProgrammableTokenTransfers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ contract ProgrammableTokenTransfers is CCIPReceiver, OwnerIsCreator {
error DestinationChainNotAllowed(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
error SourceChainNotAllowed(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
error SenderNotAllowed(address sender); // Used when the sender has not been allowlisted by the contract owner.
error InvalidReceiverAddress(); // Used when the receiver address is 0.

// Event emitted when a message is sent to another chain.
event MessageSent(
Expand Down Expand Up @@ -76,6 +77,13 @@ contract ProgrammableTokenTransfers is CCIPReceiver, OwnerIsCreator {
_;
}

/// @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 Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted.
/// @param _sourceChainSelector The selector of the destination chain.
/// @param _sender The address of the sender.
Expand Down Expand Up @@ -135,6 +143,7 @@ contract ProgrammableTokenTransfers is CCIPReceiver, OwnerIsCreator {
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Expand Down Expand Up @@ -200,6 +209,7 @@ contract ProgrammableTokenTransfers is CCIPReceiver, OwnerIsCreator {
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Expand Down
10 changes: 10 additions & 0 deletions public/samples/CCIP/TokenTransferor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ contract TokenTransferor is OwnerIsCreator {
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.
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.
// Event emitted when the tokens are transferred to an account on another chain.
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
Expand Down Expand Up @@ -53,6 +54,13 @@ contract TokenTransferor is OwnerIsCreator {
_;
}

/// @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.
Expand Down Expand Up @@ -83,6 +91,7 @@ contract TokenTransferor is OwnerIsCreator {
external
onlyOwner
onlyAllowlistedChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Expand Down Expand Up @@ -149,6 +158,7 @@ contract TokenTransferor is OwnerIsCreator {
external
onlyOwner
onlyAllowlistedChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ whatsnext:
---

import { CodeSample, ClickToZoom, CopyText, Aside } from "@components"
import CcipCommon from "@features/ccip/CcipCommon.astro"

This tutorial extends the [programmable token transfers example](/ccip/tutorials/programmable-token-transfers). It uses Chainlink CCIP to transfer tokens and arbitrary data between smart contracts on different blockchains, and focuses on defensive coding in the receiver contract. In the event of a specified error during the CCIP message reception, the contract locks the tokens. Locking the tokens allows the owner to recover and redirect them as needed. Defensive coding is crucial as it enables the recovery of locked tokens and ensures the protection of your users' assets.

<Aside type="note" title="Node Operator Rewards">
CCIP rewards the oracle node and Risk Management node operators in LINK.
</Aside>

<Aside type="caution" title="Transferring tokens">
This tutorial uses the term "transferring tokens" even though the tokens are not technically transferred. Instead,
they are locked or burned on the source chain and then unlocked or minted on the destination chain. Read the [Token
Expand All @@ -40,6 +35,12 @@ This tutorial extends the [programmable token transfers example](/ccip/tutorials

In this guide, you'll initiate a transaction from a smart contract on _Ethereum Sepolia_, sending a _string_ text and CCIP-BnM tokens to another smart contract on _Polygon Mumbai_ using CCIP. However, a deliberate failure in the processing logic will occur upon reaching the receiver contract. This tutorial will demonstrate a graceful error-handling approach, allowing the contract owner to recover the locked tokens.

<Aside type="note" title="Correctly estimate your gas limit">
It is crucial to thoroughly test all scenarios to accurately estimate the required gas limit, including for failure
scenarios. Be aware that the gas used to execute the error-handling logic for failure scenarios may be higher than
that for successful scenarios.
</Aside>

<CodeSample src="samples/CCIP/ProgrammableDefensiveTokenTransfers.sol" />

### Deploy your contracts
Expand Down Expand Up @@ -103,7 +104,7 @@ You will transfer _0.001 CCIP-BnM_ and a text. The CCIP fees for using CCIP will
| \_amount | <CopyText text="1000000000000000" code/> <br /> The token amount (_0.001 CCIP-BnM_). |

1. Click on `transact` and confirm the transaction on MetaMask.
1. After the transaction is successful, record the transaction hash. Here is an [example](https://sepolia.etherscan.io/tx/0x2cf22661edecb61978d213bd0d7ceb47bcad71cc76322c16a2158dfa42bca4a9) of a transaction on _Ethereum Sepolia_.
1. After the transaction is successful, record the transaction hash. Here is an [example](https://sepolia.etherscan.io/tx/0x24beae2612078f55ffad71f9784f0047f45cfe43e56dd16afad367f310bc5ccc) of a transaction on _Ethereum Sepolia_.

<Aside type="note">
During gas price spikes, your transaction might fail, requiring more than _0.5 LINK_ to proceed. If your
Expand All @@ -119,7 +120,7 @@ You will transfer _0.001 CCIP-BnM_ and a text. The CCIP fees for using CCIP will
alt="Chainlink CCIP Explorer transaction details"
/>

1. The CCIP transaction is completed once the status is marked as "Success". In this example, the CCIP message ID is _0x63e095fef2161d8bd3267bc474233693e85029aad3b59c12eb5b9ffdd3832f5d_.
1. The CCIP transaction is completed once the status is marked as "Success". In this example, the CCIP message ID is _0x2fb721d506350a75439b16f7dab551cf518c6dcc473b4e9271218f8f470533f8_.

<br />

Expand All @@ -132,7 +133,7 @@ You will transfer _0.001 CCIP-BnM_ and a text. The CCIP fees for using CCIP will

1. Open MetaMask and select the network _Polygon Mumbai_.
1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your smart contract deployed on _Polygon Mumbai_.
1. Call the `getFailedMessagesIds` function to list the failed messages.
1. Call the `getFailedMessages` function with an _offset_ of <CopyText text="0" code/> and a _limit_ of <CopyText text="1" code/> to retrieve the first failed message.

<br />

Expand All @@ -141,7 +142,7 @@ You will transfer _0.001 CCIP-BnM_ and a text. The CCIP fees for using CCIP will
alt="Chainlink CCIP Mumbai last failed message ids"
/>

1. Notice the messageId _0x63e095fef2161d8bd3267bc474233693e85029aad3b59c12eb5b9ffdd3832f5d_ is in the list.
1. Notice the returned values are: _0x2fb721d506350a75439b16f7dab551cf518c6dcc473b4e9271218f8f470533f8_ (the message ID) and _1_ (the error code indicating failure).

1. To recover the locked tokens, call the `retryFailedMessage` function:

Expand All @@ -166,11 +167,24 @@ You will transfer _0.001 CCIP-BnM_ and a text. The CCIP fees for using CCIP will
alt="Chainlink CCIP retry failed message - tokens transferred"
/>

1. Call again the `getFailedMessages` function with an _offset_ of <CopyText text="0" code/> and a _limit_ of <CopyText text="1" code/> to retrieve the first failed message. Notice that the error code is now _0_, indicating that the message was resolved.

<br />

<ClickToZoom
src="/images/ccip/tutorials/ccip-explorer-send-tokens-lastfailedmessageids-defensive-recovered.jpg"
alt="Chainlink CCIP retry failed message - tokens transferred - recovered"
/>

**Note**: These example contracts are designed to work bi-directionally. As an exercise, you can use them to transfer tokens with data from _Ethereum Sepolia_ to _Polygon Mumbai_ and from _Polygon Mumbai_ back to _Ethereum Sepolia_.

## Explanation

The smart contract featured in this tutorial is designed to interact with CCIP to transfer and receive tokens and data. The contract code is similar to the [_Transfer Tokens with Data_](/ccip/tutorials/programmable-token-transfers) tutorial. Hence, you can refer to its [code explanation](/ccip/tutorials/programmable-token-transfers#explanation). We will only explain the processing of received messages.
The smart contract featured in this tutorial is designed to interact with CCIP to transfer and receive tokens and data. The contract code is similar to the [_Transfer Tokens with Data_](/ccip/tutorials/programmable-token-transfers) tutorial. Hence, you can refer to its [code explanation](/ccip/tutorials/programmable-token-transfers#explanation). We will only explain the main differences.

### Sending messages

The `sendMessagePayLINK` function is similar to the `sendMessagePayLINK` function in the [_Transfer Tokens with Data_](/ccip/tutorials/programmable-token-transfers) tutorial. The main difference is the increased gas limit to account for the additional gas required to process the error-handling logic.

### Receiving and processing messages

Expand Down
Loading