diff --git a/public/images/ccip/tutorials/avalanche-usdc-messagedetails-pay-link.jpg b/public/images/ccip/tutorials/avalanche-usdc-messagedetails-pay-link.jpg deleted file mode 100644 index 3ba51f0027f..00000000000 Binary files a/public/images/ccip/tutorials/avalanche-usdc-messagedetails-pay-link.jpg and /dev/null differ diff --git a/public/images/ccip/tutorials/ccip-explorer-send-usdc-message-pay-link-tx-details-success.jpg b/public/images/ccip/tutorials/ccip-explorer-send-usdc-message-pay-link-tx-details-success.jpg index f5dbd89dcc8..d7a6db2a175 100644 Binary files a/public/images/ccip/tutorials/ccip-explorer-send-usdc-message-pay-link-tx-details-success.jpg and b/public/images/ccip/tutorials/ccip-explorer-send-usdc-message-pay-link-tx-details-success.jpg differ diff --git a/public/images/ccip/tutorials/ccip-explorer-send-usdc-message-pay-link-tx-details.jpg b/public/images/ccip/tutorials/ccip-explorer-send-usdc-message-pay-link-tx-details.jpg index cdd60988e6d..a7148b393fc 100644 Binary files a/public/images/ccip/tutorials/ccip-explorer-send-usdc-message-pay-link-tx-details.jpg and b/public/images/ccip/tutorials/ccip-explorer-send-usdc-message-pay-link-tx-details.jpg differ diff --git a/public/images/ccip/tutorials/staker-redeem-tokens-success.jpg b/public/images/ccip/tutorials/staker-redeem-tokens-success.jpg new file mode 100644 index 00000000000..55c3f5081d1 Binary files /dev/null and b/public/images/ccip/tutorials/staker-redeem-tokens-success.jpg differ diff --git a/public/images/ccip/tutorials/staker-redeem-tokens.jpg b/public/images/ccip/tutorials/staker-redeem-tokens.jpg new file mode 100644 index 00000000000..85c17b2c80f Binary files /dev/null and b/public/images/ccip/tutorials/staker-redeem-tokens.jpg differ diff --git a/public/images/ccip/tutorials/staker-tokens-balance.jpg b/public/images/ccip/tutorials/staker-tokens-balance.jpg new file mode 100644 index 00000000000..e2e53588ab8 Binary files /dev/null and b/public/images/ccip/tutorials/staker-tokens-balance.jpg differ diff --git a/public/images/ccip/tutorials/usdc-tutorial.jpg b/public/images/ccip/tutorials/usdc-tutorial.jpg new file mode 100644 index 00000000000..67d4a136b67 Binary files /dev/null and b/public/images/ccip/tutorials/usdc-tutorial.jpg differ diff --git a/public/samples/CCIP/usdc/IStaker.sol b/public/samples/CCIP/usdc/IStaker.sol new file mode 100644 index 00000000000..5fd467100b5 --- /dev/null +++ b/public/samples/CCIP/usdc/IStaker.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IStaker { + function stake(address beneficiary, uint256 amount) external; + + function redeem() external; +} diff --git a/public/samples/CCIP/usdc/Receiver.sol b/public/samples/CCIP/usdc/Receiver.sol new file mode 100644 index 00000000000..f03b527c149 --- /dev/null +++ b/public/samples/CCIP/usdc/Receiver.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol"; +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableMap} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableMap.sol"; + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ + +/// @title - A simple receiver contract for receiving usdc tokens then calling a staking contract. +contract Receiver is CCIPReceiver, OwnerIsCreator { + using SafeERC20 for IERC20; + using EnumerableMap for EnumerableMap.Bytes32ToUintMap; + + error InvalidUsdcToken(); // Used when the usdc token address is 0 + error InvalidStaker(); // Used when the staker address is 0 + error InvalidSourceChain(); // Used when the source chain is 0 + error InvalidSenderAddress(); // Used when the sender address is 0 + error NoSenderOnSourceChain(uint64 sourceChainSelector); // Used when there is no sender for a given source chain + error WrongSenderForSourceChain(uint64 sourceChainSelector); // Used when the sender contract is not the correct one + error OnlySelf(); // Used when a function is called outside of the contract itself + error WrongReceivedToken(address usdcToken, address receivedToken); // Used if the received token is different than usdc token + error CallToStakerFailed(); // Used when the call to the stake function of the staker contract is not succesful + error NoReturnDataExpected(); // Used if the call to the stake function of the staker contract returns data. This is not expected + error MessageNotFailed(bytes32 messageId); // Used if you try to retry a message that has no failed + + // Event emitted when a message is received from another chain. + event MessageReceived( + bytes32 indexed messageId, // The unique ID of the CCIP message. + uint64 indexed sourceChainSelector, // The chain selector of the source chain. + address indexed sender, // The address of the sender from the source chain. + bytes data, // The data that was received. + address token, // The token address that was transferred. + uint256 tokenAmount // The token amount that was transferred. + ); + + event MessageFailed(bytes32 indexed messageId, bytes reason); + event MessageRecovered(bytes32 indexed messageId); + + // Example error code, could have many different error codes. + enum ErrorCode { + // RESOLVED is first so that the default value is resolved. + RESOLVED, + // Could have any number of error codes here. + FAILED + } + + struct FailedMessage { + bytes32 messageId; + ErrorCode errorCode; + } + + IERC20 private immutable i_usdcToken; + address private immutable i_staker; + + // Mapping to keep track of the sender contract per source chain. + mapping(uint64 => address) public s_senders; + + // The message contents of failed messages are stored here. + mapping(bytes32 => Client.Any2EVMMessage) public s_messageContents; + + // Contains failed messages and their state. + EnumerableMap.Bytes32ToUintMap internal s_failedMessages; + + modifier validateSourceChain(uint64 _sourceChainSelector) { + if (_sourceChainSelector == 0) revert InvalidSourceChain(); + _; + } + + /// @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() { + if (msg.sender != address(this)) revert OnlySelf(); + _; + } + + /// @notice Constructor initializes the contract with the router address. + /// @param _router The address of the router contract. + /// @param _usdcToken The address of the usdc contract. + /// @param _staker The address of the staker contract. + constructor( + address _router, + address _usdcToken, + address _staker + ) CCIPReceiver(_router) { + if (_usdcToken == address(0)) revert InvalidUsdcToken(); + if (_staker == address(0)) revert InvalidStaker(); + i_usdcToken = IERC20(_usdcToken); + i_staker = _staker; + i_usdcToken.safeApprove(_staker, type(uint256).max); + } + + /// @dev Set the sender contract for a given source chain. + /// @notice This function can only be called by the owner. + /// @param _sourceChainSelector The selector of the source chain. + /// @param _sender The sender contract on the source chain . + function setSenderForSourceChain( + uint64 _sourceChainSelector, + address _sender + ) external onlyOwner validateSourceChain(_sourceChainSelector) { + if (_sender == address(0)) revert InvalidSenderAddress(); + s_senders[_sourceChainSelector] = _sender; + } + + /// @dev Delete the sender contract for a given source chain. + /// @notice This function can only be called by the owner. + /// @param _sourceChainSelector The selector of the source chain. + function deleteSenderForSourceChain( + uint64 _sourceChainSelector + ) external onlyOwner validateSourceChain(_sourceChainSelector) { + if (s_senders[_sourceChainSelector] == address(0)) + revert NoSenderOnSourceChain(_sourceChainSelector); + delete s_senders[_sourceChainSelector]; + } + + /// @notice The entrypoint for the CCIP router to call. This function should + /// never revert, all errors should be handled internally in this contract. + /// @param any2EvmMessage The message to process. + /// @dev Extremely important to ensure only router calls this. + function ccipReceive( + Client.Any2EVMMessage calldata any2EvmMessage + ) external override onlyRouter { + // validate the sender contract + if ( + abi.decode(any2EvmMessage.sender, (address)) != + s_senders[any2EvmMessage.sourceChainSelector] + ) revert WrongSenderForSourceChain(any2EvmMessage.sourceChainSelector); + /* solhint-disable no-empty-blocks */ + try this.processMessage(any2EvmMessage) { + // Intentionally empty in this example; no action needed if processMessage succeeds + } catch (bytes memory err) { + // Could set different error codes based on the caught error. Each could be + // handled differently. + s_failedMessages.set( + any2EvmMessage.messageId, + uint256(ErrorCode.FAILED) + ); + s_messageContents[any2EvmMessage.messageId] = any2EvmMessage; + // Don't revert so CCIP doesn't revert. Emit event instead. + // The message can be retried later without having to do manual execution of CCIP. + emit MessageFailed(any2EvmMessage.messageId, err); + return; + } + } + + /// @notice Serves as the entry point for this contract to process incoming messages. + /// @param any2EvmMessage Received CCIP message. + /// @dev Transfers specified token amounts to the owner of this contract. This function + /// must be external because of the try/catch for error handling. + /// It uses the `onlySelf`: can only be called from the contract. + function processMessage( + Client.Any2EVMMessage calldata any2EvmMessage + ) external onlySelf { + _ccipReceive(any2EvmMessage); // process the message - may revert + } + + function _ccipReceive( + Client.Any2EVMMessage memory any2EvmMessage + ) internal override { + if (any2EvmMessage.destTokenAmounts[0].token != address(i_usdcToken)) + revert WrongReceivedToken( + address(i_usdcToken), + any2EvmMessage.destTokenAmounts[0].token + ); + + (bool success, bytes memory returnData) = i_staker.call( + any2EvmMessage.data + ); // low level call to the staker contract using the encoded function selector and arguments + if (!success) revert CallToStakerFailed(); + if (returnData.length > 0) revert NoReturnDataExpected(); + emit MessageReceived( + any2EvmMessage.messageId, + any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector) + abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address, + any2EvmMessage.data, // received data + any2EvmMessage.destTokenAmounts[0].token, + any2EvmMessage.destTokenAmounts[0].amount + ); + } + + /// @notice Allows the owner to retry a failed message in order to unblock the associated tokens. + /// @param messageId The unique identifier of the failed message. + /// @param beneficiary The address to which the tokens will be sent. + /// @dev This function is only callable by the contract owner. It changes the status of the message + /// from 'failed' to 'resolved' to prevent reentry and multiple retries of the same message. + function retryFailedMessage( + bytes32 messageId, + address beneficiary + ) external onlyOwner { + // Check if the message has failed; if not, revert the transaction. + 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. + s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED)); + + // Retrieve the content of the failed message. + Client.Any2EVMMessage memory message = s_messageContents[messageId]; + + // This example expects one token to have been sent. + // Transfer the associated tokens to the specified receiver as an escape hatch. + IERC20(message.destTokenAmounts[0].token).safeTransfer( + beneficiary, + message.destTokenAmounts[0].amount + ); + + // Emit an event indicating that the message has been recovered. + emit MessageRecovered(messageId); + } + + /// @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 getFailedMessages( + uint256 offset, + uint256 limit + ) external view returns (FailedMessage[] memory) { + uint256 length = s_failedMessages.length(); + + // 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 failedMessages; + } +} diff --git a/public/samples/CCIP/usdc/Sender.sol b/public/samples/CCIP/usdc/Sender.sol new file mode 100644 index 00000000000..4fba2db9d89 --- /dev/null +++ b/public/samples/CCIP/usdc/Sender.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; +import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol"; +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IStaker} from "./IStaker.sol"; + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ + +/// @title - A simple messenger contract for transferring tokens to a receiver that calls a staker contract. +contract Sender is OwnerIsCreator { + using SafeERC20 for IERC20; + + // Custom errors to provide more descriptive revert messages. + error InvalidRouter(); // Used when the router address is 0 + error InvalidLinkToken(); // Used when the link token address is 0 + error InvalidUsdcToken(); // Used when the usdc token address is 0 + error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance to cover the fees. + error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw. + error InvalidDestinationChain(); // Used when the destination chain selector is 0. + error InvalidReceiverAddress(); // Used when the receiver address is 0. + error NoReceiverOnDestinationChain(uint64 destinationChainSelector); // Used when the receiver address is 0 for a given destination chain. + error AmountIsZero(); // Used if the amount to transfer is 0. + error InvalidGasLimit(); // Used if the gas limit is 0. + error NoGasLimitOnDestinationChain(uint64 destinationChainSelector); // Used when the gas limit is 0. + + // Event emitted when a message is sent to another chain. + event MessageSent( + bytes32 indexed messageId, // The unique ID of the CCIP message. + uint64 indexed destinationChainSelector, // The chain selector of the destination chain. + address indexed receiver, // The address of the receiver contract on the destination chain. + address beneficiary, // The beneficiary of the staked tokens on the destination chain. + address token, // The token address that was transferred. + uint256 tokenAmount, // The token amount that was transferred. + address feeToken, // the token address used to pay CCIP fees. + uint256 fees // The fees paid for sending the message. + ); + + IRouterClient private immutable i_router; + IERC20 private immutable i_linkToken; + IERC20 private immutable i_usdcToken; + + // Mapping to keep track of the receiver contract per destination chain. + mapping(uint64 => address) public s_receivers; + // Mapping to store the gas limit per destination chain. + mapping(uint64 => uint256) public s_gasLimits; + + modifier validateDestinationChain(uint64 _destinationChainSelector) { + if (_destinationChainSelector == 0) revert InvalidDestinationChain(); + _; + } + + /// @notice Constructor initializes the contract with the router address. + /// @param _router The address of the router contract. + /// @param _link The address of the link contract. + /// @param _usdcToken The address of the usdc contract. + constructor(address _router, address _link, address _usdcToken) { + if (_router == address(0)) revert InvalidRouter(); + if (_link == address(0)) revert InvalidLinkToken(); + if (_usdcToken == address(0)) revert InvalidUsdcToken(); + i_router = IRouterClient(_router); + i_linkToken = IERC20(_link); + i_usdcToken = IERC20(_usdcToken); + } + + /// @dev Set the receiver contract for a given destination chain. + /// @notice This function can only be called by the owner. + /// @param _destinationChainSelector The selector of the destination chain. + /// @param _receiver The receiver contract on the destination chain . + function setReceiverForDestinationChain( + uint64 _destinationChainSelector, + address _receiver + ) external onlyOwner validateDestinationChain(_destinationChainSelector) { + if (_receiver == address(0)) revert InvalidReceiverAddress(); + s_receivers[_destinationChainSelector] = _receiver; + } + + /// @dev Set the gas limit for a given destination chain. + /// @notice This function can only be called by the owner. + /// @param _destinationChainSelector The selector of the destination chain. + /// @param _gasLimit The gas limit on the destination chain . + function setGasLimitForDestinationChain( + uint64 _destinationChainSelector, + uint256 _gasLimit + ) external onlyOwner validateDestinationChain(_destinationChainSelector) { + if (_gasLimit == 0) revert InvalidGasLimit(); + s_gasLimits[_destinationChainSelector] = _gasLimit; + } + + /// @dev Delete the receiver contract for a given destination chain. + /// @notice This function can only be called by the owner. + /// @param _destinationChainSelector The selector of the destination chain. + function deleteReceiverForDestinationChain( + uint64 _destinationChainSelector + ) external onlyOwner validateDestinationChain(_destinationChainSelector) { + if (s_receivers[_destinationChainSelector] == address(0)) + revert NoReceiverOnDestinationChain(_destinationChainSelector); + delete s_receivers[_destinationChainSelector]; + } + + /// @notice Sends data and transfer tokens to receiver on the destination chain. + /// @notice Pay for fees in LINK. + /// @dev Assumes your contract has sufficient LINK to pay for CCIP fees. + /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain. + /// @param _beneficiary The address of the beneficiary of the staked tokens on the destination blockchain. + /// @param _amount token amount. + /// @return messageId The ID of the CCIP message that was sent. + function sendMessagePayLINK( + uint64 _destinationChainSelector, + address _beneficiary, + uint256 _amount + ) + external + onlyOwner + validateDestinationChain(_destinationChainSelector) + returns (bytes32 messageId) + { + address receiver = s_receivers[_destinationChainSelector]; + if (receiver == address(0)) + revert NoReceiverOnDestinationChain(_destinationChainSelector); + if (_amount == 0) revert AmountIsZero(); + uint256 gasLimit = s_gasLimits[_destinationChainSelector]; + if (gasLimit == 0) + revert NoGasLimitOnDestinationChain(_destinationChainSelector); + // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message + // address(linkToken) means fees are paid in LINK + Client.EVMTokenAmount[] + memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(i_usdcToken), + amount: _amount + }); + // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message + Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), // ABI-encoded receiver address + data: abi.encodeWithSelector( + IStaker.stake.selector, + _beneficiary, + _amount + ), // Encode the function selector and the arguments of the stake function + tokenAmounts: tokenAmounts, // The amount and type of token being transferred + extraArgs: Client._argsToBytes( + // Additional arguments, setting gas limit + Client.EVMExtraArgsV1({gasLimit: gasLimit}) + ), + // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees + feeToken: address(i_linkToken) + }); + + // Get the fee required to send the CCIP message + uint256 fees = i_router.getFee( + _destinationChainSelector, + evm2AnyMessage + ); + + if (fees > i_linkToken.balanceOf(address(this))) + revert NotEnoughBalance(i_linkToken.balanceOf(address(this)), fees); + + // approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK + i_linkToken.approve(address(i_router), fees); + + // approve the Router to spend usdc tokens on contract's behalf. It will spend the amount of the given token + i_usdcToken.approve(address(i_router), _amount); + + // Send the message through the router and store the returned message ID + messageId = i_router.ccipSend( + _destinationChainSelector, + evm2AnyMessage + ); + + // Emit an event with message details + emit MessageSent( + messageId, + _destinationChainSelector, + receiver, + _beneficiary, + address(i_usdcToken), + _amount, + address(i_linkToken), + fees + ); + + // Return the message ID + return messageId; + } + + /// @notice Allows the owner of the contract to withdraw all LINK tokens in the contract and transfer them to a beneficiary. + /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw. + /// @param _beneficiary The address to which the tokens will be sent. + function withdrawLinkToken(address _beneficiary) public onlyOwner { + // Retrieve the balance of this contract + uint256 amount = i_linkToken.balanceOf(address(this)); + + // Revert if there is nothing to withdraw + if (amount == 0) revert NothingToWithdraw(); + + i_linkToken.safeTransfer(_beneficiary, amount); + } + + /// @notice Allows the owner of the contract to withdraw all usdc tokens in the contract and transfer them to a beneficiary. + /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw. + /// @param _beneficiary The address to which the tokens will be sent. + function withdrawUsdcToken(address _beneficiary) public onlyOwner { + // Retrieve the balance of this contract + uint256 amount = i_usdcToken.balanceOf(address(this)); + + // Revert if there is nothing to withdraw + if (amount == 0) revert NothingToWithdraw(); + + i_usdcToken.safeTransfer(_beneficiary, amount); + } +} diff --git a/public/samples/CCIP/usdc/Staker.sol b/public/samples/CCIP/usdc/Staker.sol new file mode 100644 index 00000000000..83e2ca5d6d0 --- /dev/null +++ b/public/samples/CCIP/usdc/Staker.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IStaker} from "./IStaker.sol"; + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ + +/// @title - A simple Staker contract for staking usc tokens and redeeming the staker contracts +contract Staker is IStaker, ERC20 { + using SafeERC20 for ERC20; + + error InvalidUsdcToken(); // Used when the usdc token address is 0 + error InvalidNumberOfDecimals(); // Used when the number of decimals is 0 + error InvalidBeneficiary(); // Used when the beneficiary address is 0 + error InvalidAmount(); // Used when the amount is 0 + error NothingToRedeem(); // Used when the balance of Staker tokens is 0 + + event UsdcStaked(address indexed beneficiary, uint256 amount); + event UsdcRedeemed(address indexed beneficiary, uint256 amount); + + ERC20 private immutable i_usdcToken; + uint8 private immutable i_decimals; + + /// @notice Constructor initializes the contract with the usdc token address. + /// @param _usdcToken The address of the usdc contract. + constructor(address _usdcToken) ERC20("Simple Staker", "STK") { + if (_usdcToken == address(0)) revert InvalidUsdcToken(); + i_usdcToken = ERC20(_usdcToken); + i_decimals = i_usdcToken.decimals(); + if (i_decimals == 0) revert InvalidNumberOfDecimals(); + } + + function stake(address _beneficiary, uint256 _amount) external { + if (_beneficiary == address(0)) revert InvalidBeneficiary(); + if (_amount == 0) revert InvalidAmount(); + + i_usdcToken.safeTransferFrom(msg.sender, address(this), _amount); + _mint(_beneficiary, _amount); + emit UsdcStaked(_beneficiary, _amount); + } + + function redeem() external { + uint256 balance = balanceOf(msg.sender); + if (balance == 0) revert NothingToRedeem(); + _burn(msg.sender, balance); + i_usdcToken.safeTransfer(msg.sender, balance); + emit UsdcRedeemed(msg.sender, balance); + } + + function decimals() public view override returns (uint8) { + return i_decimals; + } +} diff --git a/src/content/ccip/tutorials/usdc.mdx b/src/content/ccip/tutorials/usdc.mdx index ee06760c867..8d8ae13779a 100644 --- a/src/content/ccip/tutorials/usdc.mdx +++ b/src/content/ccip/tutorials/usdc.mdx @@ -23,7 +23,7 @@ Fundamentally the [architecture](/ccip/architecture#detailed-architecture) of CC The diagram below shows that the USDC token pools and Executing DON handle the integration with Circle’s contracts and offchain CCTP Attestation API. As with any other supported ERC-20 token, USDC has a linked token pool on each supported blockchain to facilitate OnRamp and OffRamp operations. To learn more about these components, read the [architecture page](/ccip/architecture#detailed-architecture). - + The following describes the operational process: @@ -41,10 +41,25 @@ The following describes the operational process: ## Example -In this tutorial, you will send a _string_ text and USDC tokens from a smart contract on _Avalanche Fuji_ to a smart contract on _Ethereum Sepolia_. You will pay CCIP fees in LINK. -For simplicity, we will use the same contract example as the [Transfer Tokens with Data](/ccip/tutorials/programmable-token-transfers#tutorial) tutorial but for production code, we recommend to apply defensive coding (read the [Transfer Tokens With Data - Defensive Example](/ccip/tutorials/programmable-token-transfers-defensive) tutorial to learn more). +In this tutorial, you will learn how to send USDC tokens from a smart contract on Avalanche Fuji to a smart contract on Ethereum Sepolia using Chainlink CCIP and pay CCIP fees in LINK tokens. +The process uses the following steps: - + 1. **Transfer USDC and Data:** Initiate a transfer of USDC tokens and associated data from the Sender contract on Avalanche Fuji. The data includes the required arguments and the signature of the `stake` function from the Staker contract. + 1. **Receive and Stake:** The Receiver contract on Ethereum Sepolia receives the tokens and data. Then, it uses this data to make a low-level call to the Staker contract, executing the `stake` function to stake USDC on behalf of a beneficiary. + 1. **Redeem Staked Tokens:** The beneficiary can redeem the staked tokens for USDC later. + +The purpose of including the function signature and arguments in the data is to demonstrate how arbitrary data can support a variety of scenarios and use cases. By sending specific instructions within the data, you can define various interactions between smart contracts across different blockchain networks and make your decentralized application more flexible and powerful. + + + + ### Before you begin @@ -56,49 +71,104 @@ For simplicity, we will use the same contract example as the [Transfer Tokens wi ### Tutorial - - #### Deploy your contracts -To use this contract: +Deploy the Sender contract on _Avalanche Fuji_: -1. [Open the contract in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/ProgrammableTokenTransfers.sol). +1. [Open the Sender contract in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/usdc/Sender.sol&autoCompile=true). 1. Compile your contract. 1. Deploy, fund your sender contract on _Avalanche Fuji_ and enable sending messages to _Ethereum Sepolia_: 1. Open MetaMask and select the network _Avalanche Fuji_. 1. In Remix IDE, click on _Deploy & Run Transactions_ and select _Injected Provider - MetaMask_ from the environment list. Remix will then interact with your MetaMask wallet to communicate with _Avalanche Fuji_. - 1. Fill in your blockchain's router and LINK contract addresses. The router address can be found on the [supported networks page](/ccip/supported-networks) and the LINK contract address on the [LINK token contracts page](/resources/link-token-contracts). For _Avalanche Fuji_, the router address is and the LINK contract address is . + 1. Fill in your blockchain's router, LINK, and USDC contract addresses. The router and USDC addresses can be found on the [supported networks page](/ccip/supported-networks) and the LINK contract address on the [LINK token contracts page](/resources/link-token-contracts). For Avalanche Fuji, the addresses are: + + - Router address: + - LINK contract address: + - USDC contract address: + 1. Click the **transact** button. After you confirm the transaction, the contract address appears on the _Deployed Contracts_ list. Note your contract address. 1. Open MetaMask and fund your contract with USDC tokens. You can transfer _USDC_ to your contract. - 1. Fund your contract with LINK tokens. You can transfer _LINK_ to your contract. In this example, LINK is used to pay the CCIP fees. - 1. Enable your contract to send CCIP messages to _Ethereum Sepolia_: - 1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your smart contract deployed on _Avalanche Fuji_. - 1. Call the `allowlistDestinationChain` with as the destination chain selector, and as allowed. Each chain selector is found on the [supported networks page](/ccip/supported-networks). + 1. Fund your contract with LINK tokens. You can transfer _LINK_ to your contract. In this example, LINK is used to pay the CCIP fees. + +Deploy the Staker and Receiver contracts on _Ethereum Sepolia_. Configure the Receiver contract to receive CCIP messages from the Sender contract: -1. Deploy your receiver contract on _Ethereum Sepolia_ and enable receiving messages from the sender contract: +1. Deploy the Staker contract: 1. Open MetaMask and select the network _Ethereum Sepolia_. + 1. [Open the Staker contract in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/usdc/Staker.sol&autoCompile=true). + + 1. Compile your contract. 1. In Remix IDE, under _Deploy & Run Transactions_, make sure the environment is still _Injected Provider - MetaMask_. - 1. Fill in your blockchain's router and LINK contract addresses. The router address can be found on the [supported networks page](/ccip/supported-networks) and the LINK contract address on the [LINK token contracts page](/resources/link-token-contracts). For _Ethereum Sepolia_, the router address is and the LINK contract address is . + 1. Fill in the usdc contract address. The usdc contract address can be found on the [supported networks page](/ccip/supported-networks). For _Ethereum Sepolia_, the usdc contract address is: + - . 1. Click the **transact** button. After you confirm the transaction, the contract address appears on the _Deployed Contracts_ list. - Note your contract address. - 1. Enable your contract to receive CCIP messages from _Avalanche Fuji_: - 1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your smart contract deployed on _Ethereum Sepolia_. - 1. Call the `allowlistSourceChain` with as the source chain selector, and as allowed. Each chain selector is found on the [supported networks page](/ccip/supported-networks). - 1. Enable your contract to receive CCIP messages from the contract that you deployed on _Avalanche Fuji_: - 1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your smart contract deployed on _Ethereum Sepolia_. - 1. Call the `allowlistSender` with the contract address of the contract that you deployed on _Avalanche Fuji_, and as allowed. -At this point, you have one _sender_ contract on _Avalanche Fuji_ and one _receiver_ contract on _Ethereum Sepolia_. As security measures, you enabled the sender contract to send CCIP messages to _Ethereum Sepolia_ and the receiver contract to receive CCIP messages from the sender and _Avalanche Fuji_. + Note your contract address. + +1. Deploy the Receiver contract: + + 1. [Open the Receiver contract in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/usdc/Receiver.sol&autoCompile=true). + + 1. Compile your contract. + 1. In Remix IDE, under _Deploy & Run Transactions_, make sure the environment is still _Injected Provider - MetaMask_ and that you are still connected to _Ethereum Sepolia_. + 1. Fill in your blockchain's router, LINK, and Staker contract addresses. The router and usdc addresses can be found on the [supported networks page](/ccip/supported-networks) and the Staker contract address from the previous step. For _Ethereum Sepolia_, the addresses are: + + - Router address: + - USDC contract address: + - Staker address: Copied from the previous step + +1. Configure the Receiver contract to receive CCIP messages from the Sender contract: + + 1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your Receiver contract deployed on _Ethereum Sepolia_. + 1. Fill in the arguments of the _**setSenderForSourceChain**_ function: + +
+ + | Argument | Value and Description | + | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | \_sourceChainSelector |
The chain selector of _Avalanche Fuji_. You can find it on the [supported networks page](/ccip/supported-networks). | + | \_sender | Your sender contract address at _Avalanche Fuji_.
The sender contract address. | + + 1. Click on `transact` and confirm the transaction on MetaMask. + +Configure the Sender contract on _Avalanche Fuji_: + +1. Open MetaMask and select the network _Avalanche Fuji_. +1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your Sender contract deployed on _Avalanche Fuji_. +1. Fill in the arguments of the _**setReceiverForDestinationChain**_ function: + +
+ + | Argument | Value and Description | + | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | \_destinationChainSelector |
The chain selector of _Ethereum Sepolia_. You can find it on the [supported networks page](/ccip/supported-networks). | + | \_receiver | Your receiver contract address at _Ethereum Sepolia_.
The receiver contract address. | + +1. Fill in the arguments of the _**setGasLimitForDestinationChain**_: function: + +
+ + | Argument | Value and Description | + | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | \_destinationChainSelector |
The chain selector of _Ethereum Sepolia_. You can find it on the [supported networks page](/ccip/supported-networks). | + | \_gasLimit |
The gas limit for the execution of the CCIP message on the destination chain. | + +At this point: + +- You have one _sender_ contract on _Avalanche Fuji_, one _staker_ contract and one _receiver_ contract on _Ethereum Sepolia_. +- You enabled the sender contract to send messages to the receiver contract on _Ethereum Sepolia_. +- You set the gas limit for the execution of the CCIP message on _Ethereum Sepolia_. +- You enabled the receiver contract to receive messages from the sender contract on _Avalanche Fuji_. +- You funded the sender contract with USDC and LINK tokens on _Avalanche Fuji_. #### Transfer and Receive tokens and data and pay in LINK -You will transfer _1 USDC_ and a text. The CCIP fees for using CCIP will be paid in LINK. +You will transfer _1 USDC_ and arbitrary data, which contains the encoded stake function name and parameters for calling Staker's stake function on the destination chain. The parameters contain the amount of staked tokens and the beneficiary address. The CCIP fees for using CCIP will be paid in LINK. -1. Send a string data with tokens from _Avalanche Fuji_: +1. Transfer tokens and data from _Avalanche Fuji_: 1. Open MetaMask and select the network _Avalanche Fuji_. 1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your smart contract deployed on _Avalanche Fuji_. @@ -106,19 +176,17 @@ You will transfer _1 USDC_ and a text. The CCIP fees for using CCIP will be paid
- | Argument | Value and Description | - | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | \_destinationChainSelector |
CCIP Chain identifier of the destination blockchain (_Ethereum Sepolia_ in this example). You can find each chain selector on the [supported networks page](/ccip/supported-networks). | - | \_receiver | Your receiver contract address at _Ethereum Sepolia_.
The destination contract address. | - | \_text |
Any `string` | - | \_token |
The _USDC_ contract address at the source chain (_Avalanche Fuji_ in this example). You can find all the addresses for each supported blockchain on the [supported networks page](/ccip/supported-networks). | - | \_amount |
The token amount (_1 USDC_). | +| Argument | Value and Description | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| \_destinationChainSelector |
CCIP Chain identifier of the destination blockchain (_Ethereum Sepolia_ in this example). You can find each chain selector on the [supported networks page](/ccip/supported-networks). | +| \_beneficiary | The beneficiary of the Staker tokens on _Ethereum Sepolia_. You can set your own EOA (Externally Owned Account) so you can redeem the Staker tokens in exchange for USDC tokens. | +| \_amount |
The token amount (_1 USDC_). | 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://testnet.snowtrace.io/tx/0xc2094c9eff8fdf1e3656dd40a5c84bd259e2df55da0b313cb8eb621e2dda8221) of a transaction on _Avalanche Fuji_. + 1. After the transaction is successful, record the transaction hash. Here is an [example](https://testnet.snowtrace.io/tx/0x5e066ec7e94496e1547c368df4199b9f0c4f8f6c82012b2d974aa258a5c9e9fe) of a transaction on _Avalanche Fuji_. @@ -131,7 +199,7 @@ You will transfer _1 USDC_ and a text. The CCIP fees for using CCIP will be paid 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 _0x0beed86228cfe1c2685d645c911689b92725acecb01911f2aea463fd2a52156c_. +1. The CCIP transaction is completed once the status is marked as "Success". In this example, the CCIP message ID is _0xcb0fad9eec6664ad959f145cc4eb023924faded08baefc29952205ee37da7f13_.
@@ -140,19 +208,130 @@ You will transfer _1 USDC_ and a text. The CCIP fees for using CCIP will be paid alt="Chainlink CCIP Explorer transaction details success" /> -1. Check the receiver contract on the destination chain: +1. Check the balance of the beneficiary on the destination chain: 1. Open MetaMask and select the network _Ethereum Sepolia_. - 1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your smart contract deployed on _Ethereum Sepolia_. - 1. Call the `getLastReceivedMessageDetails` function. + 1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your Staker contract deployed on _Ethereum Sepolia_. + 1. Call the `balanceOf` function with the beneficiary address. + +
+ + + + 1. Notice that the balance of the beneficiary is 1,000,000 Staker tokens. The Staker contract has the same number of decimals as the USDC token, which is 6. This means the beneficiary has 1 USDC staked and can redeem it by providing the same amount of Staker tokens. + +1. Redeem the staked tokens: + + 1. Open MetaMask and make sure the network is _Ethereum Sepolia_. + 1. Make sure you are connected with the beneficiary account. + 1. In Remix IDE, under _Deploy & Run Transactions_, open the list of transactions of your Staker contract deployed on _Ethereum Sepolia_. + 1. Call the `redeem` function with the amount of Staker tokens to redeem. In this example, the beneficiary will redeem 1,000,000 Staker tokens. When confirming, MetaMask will confirm that you will transfer the Staker tokens in exchange for USDC tokens. + +
+ + + + 1. Confirm the transaction on MetaMask. After the transaction is successful, the beneficiary will receive 1 USDC tokens.
- 1. Notice the received messageId is _0x0beed86228cfe1c2685d645c911689b92725acecb01911f2aea463fd2a52156c_, the received text is _Hello World!_, the token address is _0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238_ (USDC token address on _Ethereum Sepolia_) and the token amount is 1000000 (1 USDC). +## Explanation + + + +The smart contracts featured in this tutorial are designed to interact with CCIP to send and receive USDC tokens and data across different blockchains. The contract code contains supporting comments clarifying the functions, events, and underlying logic. We will explain the Sender, Staker, and Receiver contracts further. + +### Sender Contract + + + +The Sender contract is responsible for initiating the transfer of USDC tokens and data. Here’s how it works: + +1. Initializing the contract: + + - When deploying the contract, you define the router address, LINK contract address, and USDC contract address. + - These addresses are essential for interacting with the CCIP router and handling token transfers. + +1. `sendMessagePayLINK` function: + + - This function sends USDC tokens, the encoded function signature of the `stake` function, and arguments (beneficiary address and amount) to the Receiver contract on the destination chain. + - Constructs a CCIP message using the `EVM2AnyMessage` struct. + - Computes the necessary fees using the router’s `getFee` function. + - Ensures the contract has enough LINK to cover the fees and approves the router transfer of LINK on its behalf. + - Dispatches the CCIP message to the destination chain by executing the router’s `ccipSend` function. + - Emits a `MessageSent` event. + +### Staker Contract + + + +The Staker contract manages the staking and redemption of USDC tokens. Here’s how it works: + +1. Initializing the contract: + + - When deploying the contract, you define the USDC token address. + - This address is essential for interacting with the USDC token contract. + +1. `stake` function: + + - Allows staking of USDC tokens on behalf of a beneficiary. + - Transfers USDC from the caller (`msg.sender`) to the contract, then mints an equivalent amount of staking tokens to the beneficiary. + +1. `redeem` function: + + - Allows beneficiaries to redeem their staked tokens for USDC. + - Burns the staked tokens and transfers the equivalent USDC to the beneficiary. + +### Receiver Contract + + + +The Receiver contract handles incoming cross-chain messages, processes them, and interacts with the Staker contract to stake USDC on behalf of the beneficiary. Here’s how it works: + +1. Initializing the Contract: + + - When deploying the contract, you define the router address, USDC token address, and staker contract address. + - These addresses are essential for interacting with the CCIP router, USDC token, and Staker contracts. + +1. `ccipReceive` function: + + - The entry point for the CCIP router to deliver messages to the contract. + - Validates the sender and processes the message, ensuring it comes from the correct sender contract on the source chain. + +1. Processing Message: + + - Calls the `processMessage` function, which is external to leverage Solidity’s try/catch error handling mechanism. + - Inside `processMessage`, it calls the `_ccipReceive` function for further message processing. + +1. `_ccipReceive` function: + + - Checks if the received token is USDC. If not, it reverts. + - Makes a low-level call to the `stake` function of the Staker contract using the encoded function signature and arguments from the received data. + - Emits a `MessageReceived` event upon successful processing. + +1. Error Handling: + + - If an error occurs during processing, the catch block within ccipReceive is executed. + - The `messageId` of the failed message is added to `s_failedMessages`, and the message content is stored in `s_messageContents`. + - A `MessageFailed` event is emitted, allowing for later identification and reprocessing of failed messages. + +1. `retryFailedMessage` function: + + - Allows the contract owner to retry a failed message and recover the associated tokens. + - Updates the error code for the message to `RESOLVED` to prevent multiple retries. + - Transfers the locked tokens associated with the failed message to the specified beneficiary as an escape hatch. + +1. `getFailedMessages` function: -**Note**: These example contracts are designed to work bi-directionally. You can use them as an exercise to transfer tokens with data from _Avalanche Fuji_ to _Ethereum Sepolia_ and from _Ethereum Sepolia_ back to _Avalanche Fuji_. Always ensure the sender contract on the source chain is funded with enough fee tokens. + - Retrieves a paginated list of failed messages for inspection.