Sending messages and tokens from Base to other chains using Chainlink CCIP
A tutorial that teaches how to use Chainlink CCIP to perform cross-chain messaging and token transfers from Base Goerli testnet to Optimism Goerli testnet.
Sending messages and tokens from Base to other chains using Chainlink CCIP
This tutorial will guide you through the process of sending messages and tokens from a Base smart contract to another smart contract on a different chain using Chainlink’s Cross-chain Interoperability Protocol (CCIP).
Objectives
By the end of this tutorial you should be able to do the following:
- Set up a smart contract project for Base using Foundry
- Install Chainlink CCIP as a dependency
- Use Chainlink CCIP within your smart contract to send messages and/or tokens to contracts on other different chains
- Deploy and test your smart contracts on Base testnet
Chainlink CCIP is in an “Early Access” development stage, meaning some of the functionality described within this tutorial is under development and may change in later versions.
Prerequisites
Foundry
This tutorial requires you to have Foundry installed.
- From the command-line (terminal), run:
curl -L https://foundry.paradigm.xyz | bash
- Then run
foundryup
, to install the latest (nightly) build of Foundry
For more information, see the Foundry Book installation guide.
Coinbase Wallet
In order to deploy a smart contract, you will first need a wallet. You can create a wallet by downloading the Coinbase Wallet browser extension.
- Download Coinbase Wallet
Wallet funds
For this tutorial you will need to fund your wallet with both ETH and LINK on Base Goerli and Optimism Goerli.
The ETH is required for covering gas fees associated with deploying smart contracts to the blockchain, and the LINK token is required to pay for associated fees when using CCIP.
- To fund your wallet with ETH on Base Goerli, visit a faucet listed on the Base Faucets page.
- To fund your wallet with ETH on Optimism Goerli, visit a faucet listed on the Optimism Faucets page.
- To fund your wallet with LINK, visit the Chainlink Faucet.
If you are interested in building on Mainnet, you will need to apply for Chainlink CCIP mainnet access.
What is Chainlink CCIP?
Chainlink CCIP (Cross-chain Interoperability Protocol) provides a solution for sending message data and transferring tokens across different chains.
The primary way for users to interface with Chainlink CCIP is through smart contracts known as Routers. A Router contract is responsible for initiating cross-chain interactions.
Users can interact with Routers to perform the following cross-chain capabilities:
Capability | Description | Supported receivers |
---|---|---|
Arbitrary messaging | Send arbitrary (encoded) data from one chain to another. | Smart contracts only |
Token transfers | Send tokens from one chain to another. | Smart contracts or EOAs |
Programmable token transfers | Send tokens and arbitrary (encoded) data from one chain to another, in a single transaction. | Smart contracts only |
Externally owned accounts (EOAs) on EVM blockchains are unable to receive message data, because of this, only smart contracts are supported as receivers when sending arbitrary messages or programmable token transfers. Any attempt to send a programmable token transfer (data and tokens) to an EOA, will result in only the tokens being received.
High-level concepts
Although Routers are the primary interface users will interact with when using CCIP, this section will cover what happens after instructions for a cross-chain interaction are sent to a Router.
OnRamps
Once a Router receives an instruction for a cross-chain interaction, it passes it on to another contract known as an OnRamp. OnRamps are responsible for a variety of tasks, including: verifying message size and gas limits, preserving the sequencing of messages, managing any fee payments, and interacting with the token pool to lock
or burn
tokens if a token transfer is being made.
OffRamps
The destination chain will have a contract known as an OffRamp. OffRamps are responsible for a variety of tasks, including: ensuring the authenticity of a message, making sure each transaction is only executed once, and transmitting received messages to the Router contract on the destination chain.
Token pools
A token pool is an abstraction layer over ERC-20 tokens that facilitates OnRamp and OffRamp token-related operations. They are configured to use either a Lock and Unlock
or Burn and Mint
mechanism, depending on the type of token.
For example, because blockchain-native gas tokens (i.e. ETH, MATIC, AVAX) can only be minted on their native chains, a Lock and Mint
mechanism must be used. This mechanism locks the token at the source chain, and mints a synthetic asset on the destination chain.
In contrast, tokens that can be minted on multiple chains (i.e. USDC, USDT, FRAX, etc.), token pools can use a Burn and Mint
mechanism, where the token is burnt on the source chain and minted on the destination chain.
Risk Management Network
Between instructions for a cross-chain interaction making its way from an OnRamp on the source chain to an OffRamp on the destination chain, it will pass through the Risk Management Network.
The Risk Management Network is a secondary validation service built using a variety of offchain and onchain components, with the responsibilities of monitoring all chains against abnormal activities.
A deep-dive on the technical details of each of these components is too much to cover in this tutorial, but if interested you can learn more by visiting the Chainlink documentation.
Creating a project
Before you begin, you need to set up your smart contract development environment. You can setup a development environment using tools like Hardhat or Foundry. For this tutorial you will use Foundry.
To create a new Foundry project, first create a new directory:
Then run:
This will create a Foundry project with the following basic layout:
You can delete the src/Counter.sol
, test/Counter.t.sol
, and script/Counter.s.sol
boilerplate files that were generated with the project, as you will not be needing them.
Installing Chainlink smart contracts
To use Chainlink CCIP within your Foundry project, you need to install Chainlink CCIP smart contracts as a project dependency using forge install
.
To install Chainlink CCIP smart contracts, run:
Once installed, update your foundry.toml
file by appending the following line:
Writing the smart contracts
The most basic use case for Chainlink CCIP is to send data and/or tokens between smart contracts on different blockchains.
To accomplish this, in this tutorial, you will need to create two separate smart contracts:
Sender
contract: A smart contract that interacts with CCIP to send data and tokens.Receiver
contract: A smart contract that interacts with CCIP to receive data and tokens.
Creating a Sender contract
The code snippet below is for a basic smart contract that uses CCIP to send data:
Create a new file under your project’s src/
directory named Sender.sol
and copy the code above into the file.
Code walkthrough
The sections below provide a detailed explanation for the code for the Sender
contract provided above.
Initializing the contract
In order to send data using CCIP, the Sender
contract will need access to the following dependencies:
- The
Router
contract: This contract serves as the primary interface when using CCIP to send and receive messages and tokens. - The fee token contract: This contract serves as the contract for the token that will be used to pay fees when sending messages and tokens. For this tutorial, the contract address for LINK token is used.
The Router
contract address and LINK token address are passed in as parameters to the contract’s constructor and stored as member variables for later for sending messages and paying any associated fees.
The Router
contract provides two important methods that can be used when sending messages using CCIP:
getFee
: Given a chain selector and message, returns the fee amount required to send the message.ccipSend
: Given a chain selector and message, sends the message through the router and returns an associated message ID.
The next section describes how these methods are utilized to send a message to another chain.
Sending a message
The Sender
contract defines a custom method named sendMessage
that utilizes the methods described above in order to:
- Construct a message using the
EVM2AnyMessage
method provided by theClient
CCIP library, using the following data:receiver
: The receiver contract address (encoded).data
: The text data to send with the message (encoded).tokenAmounts
: The amount of tokens to send with the message. For sending just an arbitrary message this field is defined as an empty array (new Client.EVMTokenAmount[](0)
), indicating that no tokens will be sent.extraArgs
: Extra arguments associated with the message, such asgasLimit
.feeToken
: Theaddress
of the token to be used for paying fees.
- Get the fees required to send the message using the
getFee
method provided by theRouter
contract. - Check that the contract holds an adequate amount of tokens to cover the fee. If not, revert the transaction.
- Approve the
Router
contract to transfer tokens on theSender
contracts behalf in order to cover the fees. - Send the message to a specified chain using the
Router
contract’sccipSend
method. - Return a unique ID associated with the sent message.
Creating a Receiver contract
The code snippet below is for a basic smart contract that uses CCIP to receive data:
Create a new file under your project’s src/
directory named Receiver.sol
and copy the code above into the file.
Code walkthrough
The sections below provide a detailed explanation for the code for the Receiver
contract provided above.
Initializing the contract
In order to receive data using CCIP, the Receiver
contract will need to extend to theCCIPReceiver
interface. Extending this interface allows the Receiver
contract to initialize the contract with the router address from the constructor, as seen below:
Receiving a message
Extending the CCIPReceiver
interface also allows the Receiver
contract to override the _ccipReceive
handler method for when a message is received and define custom logic.
The Receiver
contract in this tutorial provides custom logic that stores the messageId
and text
(decoded) as member variables.
Retrieving a message
The Receiver
contract defines a custom method named getMessage
that returns the details of the last received message _messageId
and _text
. This method can be called to fetch the message data details after the _ccipReceive
receives a new message.
Compiling the smart contracts
To compile your smart contracts, run:
Deploying the smart contract
Setting up your wallet as the deployer
Before you can deploy your smart contract to the Base network, you will need to set up a wallet to be used as the deployer.
To do so, you can use the cast wallet import
command to import the private key of the wallet into Foundry’s securely encrypted keystore:
After running the command above, you will be prompted to enter your private key, as well as a password for signing transactions.
For instructions on how to get your private key from Coinbase Wallet, visit the Coinbase Wallet documentation. It is critical that you do NOT commit this to a public repo.
To confirm that the wallet was imported as the deployer
account in your Foundry project, run:
Setting up environment variables
To setup your environment, create an .env
file in the home directory of your project, and add the RPC URLs, CCIP chain selectors, CCIP router addresses, and LINK token addresses for both Base Goerli and Optimism Goerli testnets:
Once the .env
file has been created, run the following command to load the environment variables in the current command line session:
Deploying the smart contracts
With your contracts compiled and environment setup, you are ready to deploy the smart contracts.
To deploy a smart contract using Foundry, you can use the forge create
command. The command requires you to specify the smart contract you want to deploy, an RPC URL of the network you want to deploy to, and the account you want to deploy with.
Your wallet must be funded with ETH on the Base Goerli and Optimism Goerli to cover the gas fees associated with the smart contract deployment. Otherwise, the deployment will fail.
To get testnet ETH for Base Goerli and Optimism Goerli, see the prerequisites.
Deploying the Sender contract to Base Goerli
To deploy the Sender
smart contract to the Base Goerli testnet, run the following command:
When prompted, enter the password that you set earlier, when you imported your wallet’s private key.
After running the command above, the contract will be deployed on the Base Goerli test network. You can view the deployment status and contract by using a [block explorer]: /docs/chain/block-explorers.
Deploying the Receiver contract to Optimism Goerli
To deploy the Receiver
smart contract to the Optimism Goerli testnet, run the following command:
When prompted, enter the password that you set earlier, when you imported your wallet’s private key.
After running the command above, the contract will be deployed on the Optimism Goerli test network. You can view the deployment status and contract by using the OP Goerli block explorer.
Funding your smart contracts
In order to pay for the fees associated with sending messages, the Sender
contract will need to hold a balance of LINK tokens.
Fund your contract directly from your wallet, or by running the following cast
command:
The above command sends 5
LINK tokens on Base Goerli testnet to the Sender
contract.
Replace SENDER_CONTRACT_ADDRESS
with the contract address of your deployed Sender contract before running the provided cast command.
Interacting with the smart contract
Foundry provides the cast
command-line tool that can be used to interact with deployed smart contracts and call their functions.
Sending data
The cast
command can be used to call the sendMessage(uint64, address, string)
function on the Sender
contract deployed to Base Goerli in order to send message data to the Receiver
contract on Optimism Goerli.
To call the sendMessage(uint64, address, string)
function of the Sender
smart contract, run:
The command above calls the sendMessage(uint64, address, string)
to send a message. The parameters passed in to the method include: The chain selector to the destination chain (Optimism Goerli), the Receiver
contract address, and the text data to be included in the message (Based
).
Replace SENDER_CONTRACT_ADDRESS
and RECEIVER_CONTRACT_ADDRESS
with the contract addresses of your deployed Sender and Receiver contracts respectively before running the provided cast command.
After running the command, a unique messageId
should be returned.
Once the transaction has been finalized, it will take a few minutes for CCIP to deliver the data to Optimism Goerli and call the ccipReceive
function on the Receiver
contract.
You can use the CCIP explorer to see the status of the CCIP transaction.
Receiving data
The cast
command can also be used to call the getMessage()
function on the Receiver
contract deployed to Optimism Goerli in order to read the received message data.
To call the getMessage()
function of the Receiver
smart contract, run:
Replace RECEIVER_CONTRACT_ADDRESS
with the contract addresses of your deployed Receiver contract before running the provided cast command.
After running the command, the messageId
and text
of the last received message should be returned.
If the transaction fails, ensure the status of your ccipSend
transaction has been finalized. You can using the CCIP explorer.
Conclusion
Congratulations! You have successfully learned how to perform cross-chain messaging on Base using Chainlink CCIP.
To learn more about cross-chain messaging and Chainlink CCIP, check out the following resources: