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
- Open a new terminal in a directory of your choice and run:
npx hardhat --init
- 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
- 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
- Create a
.envfile with the following variables:
PRIVATE_KEY=0x.....
SEPOLIA_RPC_URL=https.....
FUJI_RPC_URL=https.....
- Update
hardhat.config.tsto use your environment variables and thehardhat-viemplugin:
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
- Create a new directory named
contractsfor your smart contracts if it doesn't already exist:
mkdir -p contracts
-
Create a new file named
Sender.solin this directory and paste the sender contract code inside it. -
Create a new file named
Receiver.solin the same directory and paste the receiver contract code inside it. -
Create a
contracts/interfacesdirectory and create a new file namedIERC20.solinside 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 calltransfer.
mkdir -p contracts/interfaces
- 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
- Create a new directory named
scriptsat the root of the project if it doesn't already exist:
mkdir -p scripts
- Create a new file named
send-cross-chain-message.tsin 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."
)
- 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.tsin thescriptsdirectory 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
- 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
- Run the following command to send the cross-chain message:
npx hardhat run scripts/send-cross-chain-message.ts
- Wait a few minutes for the message to be delivered to the receiver contract.
6 Verify cross-chain delivery
- Paste the Receiver contract address into
RECEIVER_ADDRESSinscripts/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