Get Started (EVM)

Build and run a secure cross-chain messaging workflow between two EVM chains using Chainlink CCIP.

By the end of this guide, you will:

  • Deploy a sender on a source chain
  • Deploy a receiver on a destination chain
  • Send and verify a cross-chain message

Before you begin

You will need:

  • Basic Solidity and smart contract deployment experience

  • One wallet funded on two CCIP-supported EVM testnets

  • Choose one of the following development environments:

    • Hardhat (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.

    • Foundry Best for Solidity-native, test-driven workflows, where CCIP messaging is validated through tests and assertions rather than scripts.

    • Remix Suitable only for quick, disposable demos. Not recommended for testing, iteration, or production workflows.

Build and send a message

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 Bootstrap the Hardhat project
  1. Open a new terminal in a directory of your choice and run:
npx hardhat --init
  1. Create a project with the following options:
  • Hardhat Version: hardhat-3
  • Initialize project: At root of the project
  • Type of project: A minimal Hardhat project
  • Install the necessary dependencies: Yes
  1. Install the additional dependencies required by this tutorial:
npm install @chainlink/contracts-ccip @chainlink/contracts @openzeppelin/contracts viem dotenv
npm install --save-dev @nomicfoundation/hardhat-viem
  1. Create a .env file with the following variables:
PRIVATE_KEY=0x.....
SEPOLIA_RPC_URL=https.....
FUJI_RPC_URL=https.....
  1. Update hardhat.config.ts to use your environment variables and the hardhat-viem plugin:
import "dotenv/config"
import { configVariable, defineConfig } from "hardhat/config"
import hardhatViem from "@nomicfoundation/hardhat-viem"

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")],
    },
  },
})
2 Add the CCIP contracts
  1. Create a new directory named contracts for your smart contracts if it doesn't already exist:
mkdir -p contracts
  1. Create a new file named Sender.sol in this directory and paste the sender contract code inside it.

  2. Create a new file named Receiver.sol in the same directory and paste the receiver contract code inside it.

  3. 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.

mkdir -p contracts/interfaces
  1. Paste the following code into contracts/interfaces/IERC20.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// Re-export OpenZeppelin's IERC20 interface
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IERC20Extended is IERC20 {}

Run the following command to compile the contracts:

npx hardhat build
3 Set up the scripts
  1. Create a new directory named scripts at the root of the project if it doesn't already exist:
mkdir -p scripts
  1. Create a new file named send-cross-chain-message.ts in this directory and paste the following code inside it:
import { network } from "hardhat"
import { parseUnits } from "viem"

// Avalanche Fuji configuration
const FUJI_ROUTER = "0xF694E193200268f9a4868e4Aa017A0118C9a8177"
const FUJI_LINK = "0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846"

// 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

// 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")

// Step 1: Deploy Sender on Fuji
console.log("\n[Step 1] Deploying Sender contract on Avalanche Fuji...")

const sender = await fujiNetwork.viem.deployContract("Sender", [FUJI_ROUTER, FUJI_LINK])
const fujiPublicClient = await fujiNetwork.viem.getPublicClient()

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}`)

// 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)

const transferLinkToFujiContract = await fujiLinkToken.write.transfer([sender.address, parseUnits("1", 18)])

console.log("LINK token transfer in progress, awaiting confirmation...")
await fujiPublicClient.waitForTransactionReceipt({ hash: transferLinkToFujiContract, confirmations: 1 })
console.log(`Funded Sender with 1 LINK`)

// Step 3: Deploy Receiver on Sepolia
console.log("\n[Step 3] Deploying Receiver on Ethereum Sepolia...")

const receiver = await sepoliaNetwork.viem.deployContract("Receiver", [SEPOLIA_ROUTER])
const sepoliaPublicClient = await sepoliaNetwork.viem.getPublicClient()

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`)

// Step 4: Send cross-chain message
console.log("\n[Step 4] Sending cross-chain message...")

const sendMessageTx = await sender.write.sendMessage([
  SEPOLIA_CHAIN_SELECTOR,
  receiver.address,
  "Hello World! cdnjkdjmdsd",
])

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."
)
  1. 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:
import { network } from "hardhat"

// Paste the Receiver contract address
const RECEIVER_ADDRESS = ""

console.log("Connecting to Ethereum Sepolia...")
const sepoliaNetwork = await network.connect("sepolia")

console.log("Checking for received message...\n")
const receiver = await sepoliaNetwork.viem.getContractAt("Receiver", RECEIVER_ADDRESS)

const [messageId, text] = await receiver.read.getLastReceivedMessageDetails()

// A null hexadecimal value means no message has been received yet
const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"

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}"`)
}
4 Deploy the contracts
  1. Run the following command to deploy the contracts:
npx hardhat run scripts/send-cross-chain-message.ts

The script logs the deployed contract addresses to the terminal.

5 Send a CCIP message
  1. Run the following command to send the cross-chain message:
npx hardhat run scripts/send-cross-chain-message.ts
  1. Wait a few minutes for the message to be delivered to the receiver contract.
6 Verify cross-chain delivery
  1. Paste the Receiver contract address into RECEIVER_ADDRESS in scripts/verify-cross-chain-message.ts, then run:
npx hardhat run scripts/verify-cross-chain-message.ts

You should see the message ID and text of the last received message printed in the terminal.

Foundry

Use this path if you prefer Solidity-native workflows and want to verify CCIP messaging using tests and assertions.

....

Remix

Use this path only for quick experimentation. This path is not suitable for testing or production use. For real applications, migrate to Hardhat or Foundry.

....

Common issues and debugging

If your message does not execute as expected, check the following:

  • Incorrect router address or chain selector
  • Insufficient fee payment on the source chain
  • Receiver execution revert
  • Message still in transit (cross-chain delivery is not instant)

Most issues are configuration-related and can be resolved by verifying network-specific values.

Next steps

After completing this guide, you can:

  • Send tokens cross-chain with CCIP
  • Add automated tests to your project
  • Monitor message status programmatically
  • Deploy CCIP applications to mainnet

What's next

Get the latest Chainlink content straight to your inbox.