diff --git a/src/content/ccip/getting-started/evm.mdx b/src/content/ccip/getting-started/evm.mdx index 96d0822ddee..593ad75e495 100644 --- a/src/content/ccip/getting-started/evm.mdx +++ b/src/content/ccip/getting-started/evm.mdx @@ -1,7 +1,7 @@ --- section: ccip date: Last Modified -title: "Getting Started (EVM)" +title: "Get Started (EVM)" metadata: description: "Get started with Chainlink CCIP on EVM chains. Deploy sender/receiver contracts, send data cross-chain, and pay fees in LINK." excerpt: "Chainlink CCIP, EVM chains, Ethereum, Avalanche, Cross-chain messaging, Solidity contracts, LINK token, Remix IDE, MetaMask, Testnet" @@ -18,214 +18,331 @@ whatsnext: } --- -import { CodeSample, ClickToZoom, CopyText, Aside } from "@components" +import { Accordion, CodeSample, ClickToZoom, CopyText, Aside } from "@components" import CcipCommon from "@features/ccip/CcipCommon.astro" - +**Build and run a secure cross-chain messaging workflow between two EVM chains using Chainlink CCIP.** -A simple use case for Chainlink CCIP is sending data between smart contracts on different blockchains. This guide shows you how to deploy a CCIP sender contract and a CCIP receiver contract to two different blockchains and send data from the sender contract to the receiver contract. You pay the CCIP fees using LINK. +By the end of this guide, you will: -Fees can also be paid in alternative assets, which currently include the native gas tokens of the source blockchain and their ERC20 wrapped version. For example, you can pay ETH or WETH when you send transactions to the CCIP router on Ethereum and AVAX or WAVAX when you send transactions to the CCIP router on Avalanche. +- Deploy a sender on a source chain +- Deploy a receiver on a destination chain +- Send and verify a cross-chain message ## Before you begin -- If you are new to smart contract development, learn how to [Deploy Your First Smart Contract](/quickstarts/deploy-your-first-contract) so you are familiar with the tools that are necessary for this guide: - - The [Solidity](https://soliditylang.org/) programming language - - The [MetaMask](https://metamask.io) wallet - - The [Remix](https://remix.ethereum.org/) development environment -- Acquire testnet funds. This guide requires testnet AVAX and LINK on _Avalanche Fuji_. It also requires testnet ETH on _Ethereum Sepolia_. If you need to use different networks, you can find more faucets on the [LINK Token Contracts](/resources/link-token-contracts) page. - - Go to [faucets.chain.link](https://faucets.chain.link/) to get your testnet tokens. -- Learn how to [Fund your contract with LINK](/resources/fund-your-contract). +You will need: -## Deploy the sender contract +- Basic [Solidity](https://soliditylang.org/) and [smart contract deployment](/quickstarts/deploy-your-first-contract) experience +- One [wallet](https://metamask.io/) funded on two CCIP-supported EVM testnets -Deploy the `Sender.sol` contract on _Avalanche Fuji_. To see a detailed explanation of this contract, read the [Code Explanation](#sender-code) section. +- Choose one of the following development environments: + - **[Hardhat](https://hardhat.org/docs/getting-started) (recommended)** + Best for a **script-driven workflow** where you deploy contracts, send a CCIP message, and verify delivery from the command line. Ideal for getting a full end-to-end CCIP flow running quickly. -1. [Open the Sender.sol contract](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/Sender.sol) in Remix. + - **[Foundry](https://book.getfoundry.sh/)** + Best for **Solidity-native, test-driven workflows**, where CCIP messaging is validated through tests and assertions rather than scripts. - + - **[Remix](https://remix.ethereum.org/)** + Suitable only for **quick, disposable demos**. Not recommended for testing, iteration, or production workflows. -1. Compile the contract. +## Build and send a message -1. Deploy the sender contract on _Avalanche Fuji_: - 1. Open MetaMask and select the _Avalanche Fuji_ network. - 1. In Remix under the **Deploy & Run Transactions** tab, select _Injected Provider - MetaMask_ in the **Environment** list. Remix will use the MetaMask wallet to communicate with _Avalanche Fuji_. - 1. Under the **Deploy** section, fill in the router address and the LINK token contract addresses for your specific blockchain. You can find both of these addresses on the [CCIP Directory](/ccip/directory). The LINK token contract address is also listed on the [LINK Token Contracts](/resources/link-token-contracts) page. For _Avalanche Fuji_, the router address is and the LINK address is . +### Hardhat - +In this section, you will use preconfigured Hardhat scripts to deploy both contracts, send a CCIP message, and verify cross-chain delivery from the command line. - 1. Click the **transact** button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to _Avalanche Fuji_. +
- 1. After you confirm the transaction, the contract address appears in the **Deployed Contracts** list. Copy your contract address. + +1. Open a new terminal in a directory of your choice and run: - +```bash +npx hardhat --init +``` - 1. Open MetaMask and send LINK to the contract address that you copied. Your contract will pay CCIP fees in LINK. +2. Create a project with the following options: - **Note:** This transaction fee is significantly higher than normal due to gas spikes on Sepolia. To run this example, you can get additional testnet LINK - from [faucets.chain.link](https://faucets.chain.link) or use a supported testnet other than Sepolia. +- **Hardhat Version:** `hardhat-3` +- **Initialize project:** At root of the project +- **Type of project:** A minimal Hardhat project +- **Install the necessary dependencies:** Yes -## Deploy the receiver contract +3. Install the additional dependencies required by this tutorial: -Deploy the receiver contract on _Ethereum Sepolia_. You will use this contract to receive data from the sender that you deployed on _Avalanche Fuji_. To see a detailed explanation of this contract, read the [Code Explanation](#receiver-code) section. +```bash +npm install @chainlink/contracts-ccip @chainlink/contracts @openzeppelin/contracts viem dotenv +npm install --save-dev @nomicfoundation/hardhat-viem +``` -1. [Open the Receiver.sol](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/Receiver.sol) contract in Remix. + + +4. Create a `.env` file with the following variables: - +```bash +PRIVATE_KEY=0x..... +SEPOLIA_RPC_URL=https..... +FUJI_RPC_URL=https..... +``` -1. Compile the contract. +5. Update `hardhat.config.ts` to use your environment variables and the `hardhat-viem` plugin: -1. Deploy the receiver contract on _Ethereum Sepolia_: - 1. Open MetaMask and select the _Ethereum Sepolia_ network. +```ts +import "dotenv/config" +import { configVariable, defineConfig } from "hardhat/config" +import hardhatViem from "@nomicfoundation/hardhat-viem" - 1. In Remix under the **Deploy & Run Transactions** tab, make sure the **Environment** is still set to _Injected Provider - MetaMask_. - 1. Under the **Deploy** section, fill in the router address field. For _Ethereum Sepolia_, the Router address is . You can find the addresses for each network on the [CCIP Directory](/ccip/directory). +export default defineConfig({ + plugins: [hardhatViem], + solidity: { + version: "0.8.24", + }, + networks: { + sepolia: { + type: "http", + url: configVariable("SEPOLIA_RPC_URL"), + accounts: [configVariable("PRIVATE_KEY")], + }, + avalancheFuji: { + type: "http", + url: configVariable("FUJI_RPC_URL"), + accounts: [configVariable("PRIVATE_KEY")], + }, + }, +}) +``` - + - 1. Click the **Deploy** button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to _Ethereum Sepolia_. + +1. Create a new directory named `contracts` for your smart contracts if it doesn't already exist: - 1. After you confirm the transaction, the contract address appears as the second item in the **Deployed Contracts** list. Copy this contract address. +```bash +mkdir -p contracts +``` - +2. Create a new file named `Sender.sol` in this directory and paste the sender contract code inside it. -You now have one _sender_ contract on _Avalanche Fuji_ and one _receiver_ contract on _Ethereum Sepolia_. You sent `70` LINK to the _sender_ contract to pay the CCIP fees. Next, send data from the sender contract to the receiver contract. +3. Create a new file named `Receiver.sol` in the same directory and paste the receiver contract code inside it. -## Send data +4. Create a `contracts/interfaces` directory and create a new file named `IERC20.sol` inside it. Our script will need to make a call to the LINK ERC-20 contract to transfer LINK to the sender contract, so it needs an ERC-20 interface to call `transfer`. -Send a `Hello World!` string from your contract on _Avalanche Fuji_ to the contract you deployed on _Ethereum Sepolia_: +```bash +mkdir -p contracts/interfaces +``` -1. Open MetaMask and select the _Avalanche Fuji_ network. -1. In Remix under the **Deploy & Run Transactions** tab, expand the first contract in the **Deployed Contracts** section. -1. Expand the **sendMessage** function and fill in the following arguments: +5. Paste the following code into `contracts/interfaces/IERC20.sol`: - | Argument | Description | Value (_Ethereum Sepolia_) | - | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | - | destinationChainSelector | CCIP Chain identifier of the target blockchain. You can find each network's chain selector on the [CCIP Directory](/ccip/directory) | | - | receiver | The destination smart contract address | Your deployed contract address | - | text | Any `string` | | +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; - +// Re-export OpenZeppelin's IERC20 interface +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -1. Click the **transact** button to run the function. MetaMask prompts you to confirm the transaction. +interface IERC20Extended is IERC20 {} +``` - +Run the following command to compile the contracts: -1. After the transaction is successful, note the transaction hash. Here is an [example](https://testnet.snowtrace.io/tx/0x113933ec9f1b2e795a1e2f564c9d452db92d3e9a150545712687eb546916e633) of a successful transaction on _Avalanche Fuji_. +```bash +npx hardhat build +``` -After the transaction is finalized on the source chain, it will take a few minutes for CCIP to deliver the data to _Ethereum Sepolia_ and call the `ccipReceive` function on your receiver contract. You can use the [CCIP explorer](https://ccip.chain.link/) to see the status of your CCIP transaction and then read data stored by your receiver contract. + -1. Open the [CCIP explorer](https://ccip.chain.link/) and use the transaction hash that you copied to search for your cross-chain transaction. The explorer provides several details about your request. + +1. Create a new directory named `scripts` at the root of the project if it doesn't already exist: - +```bash +mkdir -p scripts +``` -1. When the status of the transaction is marked with a "Success" status, the CCIP transaction and the destination transaction are complete. +2. Create a new file named `send-cross-chain-message.ts` in this directory and paste the following code inside it: - +```ts +import { network } from "hardhat" +import { parseUnits } from "viem" -## Read data +// Avalanche Fuji configuration +const FUJI_ROUTER = "0xF694E193200268f9a4868e4Aa017A0118C9a8177" +const FUJI_LINK = "0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846" -Read data stored by the receiver contract on _Ethereum Sepolia_: +// Ethereum Sepolia configuration +// Note that the contract on Sepolia doesn't need to have LINK to pay for CCIP fees. +const SEPOLIA_ROUTER = "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59" +const SEPOLIA_CHAIN_SELECTOR = 16015286601757825753n -1. Open MetaMask and select the _Ethereum Sepolia_ network. -1. In Remix under the **Deploy & Run Transactions** tab, expand the receiver contract deployed on _Ethereum Sepolia_. -1. Click the **getLastReceivedMessageDetails** function button to read the stored data. In this example, it is "Hello World!". +// Connect to Avalanche Fuji +console.log("Connecting to Avalanche Fuji...") +const fujiNetwork = await network.connect("avalancheFuji") - +// Connect to Ethereum Sepolia +console.log("Connecting to Ethereum Sepolia...") +const sepoliaNetwork = await network.connect("sepolia") -Congratulations! You just sent your first cross-chain data using CCIP. Next, examine the example code to learn how this contract works. +// Step 1: Deploy Sender on Fuji +console.log("\n[Step 1] Deploying Sender contract on Avalanche Fuji...") -## Examine the example code +const sender = await fujiNetwork.viem.deployContract("Sender", [FUJI_ROUTER, FUJI_LINK]) +const fujiPublicClient = await fujiNetwork.viem.getPublicClient() -### Sender code +console.log(`Sender contract has been deployed to this address on the Fuji testnet: ${sender.address}`) +console.log(`View on Avascan: https://testnet.avascan.info/blockchain/all/address/${sender.address}`) -The smart contract in this tutorial is designed to interact with CCIP to send data. The contract code includes comments to clarify the various functions, events, and underlying logic. However, this section explains the key elements. You can see the full contract code below. +// Step 2: Fund Sender with LINK +console.log("\n[Step 2] Funding Sender with 1 LINK...") - +const fujiLinkToken = await fujiNetwork.viem.getContractAt("IERC20Extended", FUJI_LINK) -#### Initializing the contract +const transferLinkToFujiContract = await fujiLinkToken.write.transfer([sender.address, parseUnits("1", 18)]) -When deploying the contract, you define the router address and the LINK contract address of the blockchain where you choose to deploy the contract. +console.log("LINK token transfer in progress, awaiting confirmation...") +await fujiPublicClient.waitForTransactionReceipt({ hash: transferLinkToFujiContract, confirmations: 1 }) +console.log(`Funded Sender with 1 LINK`) -The router address provides functions that are required for this example: +// Step 3: Deploy Receiver on Sepolia +console.log("\n[Step 3] Deploying Receiver on Ethereum Sepolia...") -- The `getFee` [function](/ccip/api-reference/evm/v1.6.1/i-router-client#getfee) to estimate the CCIP fees. -- The `ccipSend` [function](/ccip/api-reference/evm/v1.6.1/i-router-client#ccipsend) to send CCIP messages. +const receiver = await sepoliaNetwork.viem.deployContract("Receiver", [SEPOLIA_ROUTER]) +const sepoliaPublicClient = await sepoliaNetwork.viem.getPublicClient() -#### Sending data +console.log(`Receiver contract has been deployed to this address on the Sepolia testnet: ${receiver.address}`) +console.log(`View on Etherscan: https://sepolia.etherscan.io/address/${receiver.address}`) +console.log(`\nšŸ“‹ Copy the receiver address since it will be needed to run the verification script šŸ“‹ \n`) -The `sendMessage` function completes several operations: +// Step 4: Send cross-chain message +console.log("\n[Step 4] Sending cross-chain message...") -1. Construct a CCIP-compatible message using the `EVM2AnyMessage` [struct](/ccip/api-reference/evm/v1.6.1/client#evm2anymessage): - - The `receiver` address is encoded in bytes format to accommodate non-EVM destination blockchains with distinct address formats. The encoding is achieved through [abi.encode](https://docs.soliditylang.org/en/develop/abi-spec.html). - - The `data` is encoded from a string text to bytes using [abi.encode](https://docs.soliditylang.org/en/develop/abi-spec.html). - - The `tokenAmounts` is an array. Each element comprises a [struct](/ccip/api-reference/evm/v1.6.1/client#evmtokenamount) that contains the token address and amount. In this example, the array is empty because no tokens are sent. - - The `extraArgs` specify the `gasLimit` for relaying the CCIP message to the recipient contract on the destination blockchain. In this example, the `gasLimit` is set to `200000`. - - The `feeToken` designates the token address used for CCIP fees. Here, `address(linkToken)` signifies payment in LINK. +const sendMessageTx = await sender.write.sendMessage([ + SEPOLIA_CHAIN_SELECTOR, + receiver.address, + "Hello World! cdnjkdjmdsd", +]) -1. Compute the fees by invoking the router's `getFee` [function](/ccip/api-reference/evm/v1.6.1/i-router-client#getfee). -1. Ensure that your contract balance in LINK is enough to cover the fees. -1. Grant the router contract permission to deduct the fees from the contract's LINK balance. -1. Dispatch the CCIP message to the destination chain by executing the router's `ccipSend` [function](/ccip/api-reference/evm/v1.6.1/i-router-client#ccipsend). +console.log("Cross-chain message sent, awaiting confirmation...") +console.log(`Message sent from source contract! āœ… \n Tx hash: ${sendMessageTx}`) +console.log(`View transaction status on CCIP Explorer: https://ccip.chain.link`) +console.log( + "Run the receiver script after 10 minutes to check if the message has been received on the destination contract." +) +``` - +3. Wait for a few minutes for the message to be delivered to the receiver contract. Then create a new file named `verify-cross-chain-message.ts` in the `scripts` directory and paste the following code inside it: -### Receiver code + -The smart contract in this tutorial is designed to interact with CCIP to receive data. The contract code includes comments to clarify the various functions, events, and underlying logic. However, this section explains the key elements. You can see the full contract code below. +```ts +import { network } from "hardhat" - +// Paste the Receiver contract address +const RECEIVER_ADDRESS = "" -#### Initializing the contract +console.log("Connecting to Ethereum Sepolia...") +const sepoliaNetwork = await network.connect("sepolia") -When you deploy the contract, you define the router address. The receiver contract inherits from the [CCIPReceiver.sol](/ccip/api-reference/evm/v1.6.1/ccip-receiver) contract, which uses the router address. +console.log("Checking for received message...\n") +const receiver = await sepoliaNetwork.viem.getContractAt("Receiver", RECEIVER_ADDRESS) -#### Receiving data +const [messageId, text] = await receiver.read.getLastReceivedMessageDetails() -On the destination blockchain: +// A null hexadecimal value means no message has been received yet +const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000" -1. The CCIP Router invokes the `ccipReceive` [function](/ccip/api-reference/evm/v1.6.1/ccip-receiver#ccipreceive). **Note**: This function is protected by the `onlyRouter` [modifier](/ccip/api-reference/evm/v1.6.1/ccip-receiver#onlyrouter), which ensures that only the router can call the receiver contract. -1. The `ccipReceive` [function](/ccip/api-reference/evm/v1.6.1/ccip-receiver#ccipreceive) calls an internal function `_ccipReceive` [function](/ccip/api-reference/evm/v1.6.1/ccip-receiver#_ccipreceive). The receiver contract implements this function. -1. This `_ccipReceive` [function](/ccip/api-reference/evm/v1.6.1/ccip-receiver#_ccipreceive) expects an `Any2EVMMessage` [struct](/ccip/api-reference/evm/v1.6.1/client#any2evmmessage) that contains the following values: - - The CCIP `messageId`. - - The `sourceChainSelector`. - - The `sender` address in bytes format. The sender is a contract deployed on an EVM-compatible blockchain, so the address is decoded from bytes to an Ethereum address using the [ABI specification](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html). - - The `data` is also in bytes format. A `string` is expected, so the data is decoded from bytes to a string using the [ABI specification](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html). +if (messageId === ZERO_BYTES32) { + console.log("No message received yet.") + console.log("Please wait a bit longer and try again.") + process.exit(1) +} else { + console.log(`āœ… Message ID: ${messageId}`) + console.log(`Text: "${text}"`) +} +``` -