Hello! In our last lesson, we navigated the theoretical landscape of cross-chain communication, exploring different architectures like trusted, trust-minimized, and optimistic protocols, and outlining a framework for analyzing their risks. We learned that generic messaging protocols like LayerZero and Chainlink CCIP are typically built on a "Third-Party Attestation" model, where a set of external actors validates cross-chain messages.
Today, we transition from theory to practice. Our goal is to implement the first half of a cross-chain interaction: the sender. We will write a smart contract that prepares a message and dispatches it from a source chain, destined for another blockchain. By the end of this lesson, you will be able to implement a simple contract that uses a generic messaging protocol to send a message, focusing on the sender's responsibilities.
From Architecture to Action
Let's quickly recall the high-level architecture of a third-party messaging protocol.

Our sender contract's job isn't to handle the complex off-chain communication itself. Instead, it interacts with a dedicated on-chain contract provided by the protocol (an Endpoint for LayerZero, a Router for Chainlink CCIP). Our contract is responsible for a few key tasks:
- Constructing the Payload: Defining the actual data (the message) to be sent across chains.
- Specifying the Destination: Providing the protocol with the target chain and the address of the recipient contract.
- Paying for the Service: Attaching a fee to cover the costs of validation and transaction execution on the destination chain.
- Initiating the Send: Calling the appropriate function on the protocol's on-chain contract to kick off the process.
We will now explore how to implement this using two of the most prominent messaging protocols: LayerZero and Chainlink CCIP.
Implementing a Sender with LayerZero
LayerZero is an "omnichain interoperability protocol" that provides a generic messaging layer. An application interacts with a single Endpoint contract on each chain.
The following tutorial uses Hardhat to demonstrate a basic LayerZero contract. While the example contract acts as both a sender and receiver, we will concentrate on the sending logic.
LayerZero Tutorial for Beginners
This article from Tim4l1f3 provides a concise example of a LayerZero contract. Focus on understanding the structure of the sendMsg function and how it interacts with the LayerZero Endpoint.
First, review the complete LayerZeroDemo1.sol contract code. Then, read the explanation of the key functions that follows it. Pay special attention to the constructor and sendMsg function. We will cover the lzReceive function in our next lesson.
Code Deconstruction: The LayerZero Sender
Let's break down the essential sender-side components from the tutorial:
-
constructor(address _endpoint): The contract is initialized with the address of the official LayerZero Endpoint for its native chain. This endpoint is our contract's sole gateway to the LayerZero network. You can find a list of official endpoint addresses in the LayerZero documentation. -
function sendMsg(uint16 _dstChainId, bytes calldata _destination, bytes calldata payload) public payable: This is our application-specific function to initiate a cross-chain message._dstChainId: This is a LayerZero-specific identifier for the destination chain (e.g.,10009for Polygon Mumbai)._destination: This is the address of our receiver contract on the destination chain. It must be encoded asbytesfor thesendfunction.payload: The arbitrary data we want to send.
-
endpoint.send{value: msg.value}(...): This is the core of the interaction.- Our contract calls the
sendfunction on theEndpointcontract we stored in the constructor. - Crucially, we use
payableand{value: msg.value}. This forwards the native currency (e.g., ETH, MATIC) sent to oursendMsgfunction to the LayerZero endpoint. This payment covers the fees for the Oracle, the Relayer, and the gas costs for the transaction on the destination chain. - The
estimateFeesfunction can be called beforehand to determine the requiredmsg.valuefor a given message, ensuring the transaction doesn't fail due to insufficient fees.
- Our contract calls the
Implementing a Sender with Chainlink CCIP
Chainlink's Cross-Chain Interoperability Protocol (CCIP) is another powerful solution for building cross-chain applications. It uses a Router contract as the main on-chain entry point and has a sophisticated off-chain network for validation and execution.
The following tutorial provides a clear example of building sender and receiver contracts using the Foundry toolchain. This separation of concerns aligns perfectly with our learning path.
Chainlink CCIP Tutorial: Base Goerli to Optimism Goerli
Next, let's see how Chainlink CCIP tackles the same problem. This official tutorial from the Base documentation uses Foundry and creates a dedicated Sender.sol contract, giving us a clean look at the sender's responsibilities.
Read the sections 'Writing the smart contracts', 'Initializing the contract' (under Code walkthrough), and 'Sending a message'. This will guide you through the complete logic of a CCIP sender, from setup to execution.
Code Deconstruction: The CCIP Sender
The CCIP approach is slightly more structured, breaking the process into explicit steps:
-
constructor(address _router, address _link): The CCIPSenderis initialized with the addresses of the CCIPRouterfor the source chain and theLINKtoken contract. Fees for CCIP services are paid inLINK. -
function sendMessage(uint64 _destinationChainSelector, address _receiver, string calldata _text): This function orchestrates the sending process.- Construct the Message: The
Client.EVM2AnyMessagestruct is used to package the message payload (_text) and other parameters. This strongly-typed approach helps prevent errors. - Calculate the Fee: It calls
router.getFee(...)to determine the exactLINKtoken amount required to send the specified message to the destination chain. - Approve Fee Payment: It calls
linkToken.approve(address(router), fee). This is a standard ERC-20 pattern that gives the CCIPRoutercontract permission to withdraw the calculated fee amount from ourSendercontract. - Send the Message: Finally, it calls
router.ccipSend(...), passing the destination chain selector and the packaged message. The Router then pulls the approved fee and begins the cross-chain process.
- Construct the Message: The
Comparing the Sender Implementations
At a high level, both protocols achieve the same outcome, but their APIs have important differences. Your experience as a developer in choosing and integrating libraries for different tasks is directly applicable here.
| Feature | LayerZero (Example) | Chainlink CCIP (Example) |
|---|---|---|
| On-Chain Interface | ILayerZeroEndpoint | IRouterClient |
| Chain Identifier | uint16 chain ID | uint64 chain selector |
| Fee Payment | Native currency (e.g., ETH) via msg.value | LINK tokens (or native) via ERC-20 approve |
| Core Send Function | endpoint.send(...) | router.ccipSend(...) |
| Fee Calculation | endpoint.estimateFees(...) | router.getFee(...) |
| Message Structure | Raw bytes payload | Client.EVM2AnyMessage struct |
Understanding these patterns is key. While the function names and fee mechanisms differ, the fundamental developer workflow of preparing a message, calculating/paying a fee, and calling a protocol-specific function remains consistent.
Test your understanding!
You're tasked with writing a JavaScript script using ethers.js to call the sendMessage function on the Chainlink CCIP Sender contract we just reviewed. Based on the contract's logic, what three essential pieces of information or pre-conditions must be in place before your script can successfully execute the sendMessage transaction? Assume your contract is already deployed.
Show answer
- Destination Chain Selector: You need the
uint64value that uniquely identifies the target blockchain in the CCIP network (e.g., the selector for Optimism Goerli). This is a required parameter for the function. - Receiver Contract Address: You need the
addressof the contract on the destination chain that is intended to receive the message. This is another required function parameter. - Sufficient LINK Balance: The
Sendercontract itself must be funded with enoughLINKtokens to cover the fee that will be calculated byrouter.getFee(). Without this balance, thelinkToken.approvecall will succeed, but the finalccipSendwill fail when the Router tries to pull the fee.
Conclusion
Today we took a significant step from abstract architectural diagrams to concrete Solidity code. We've seen how a developer can leverage complex cross-chain messaging protocols through straightforward on-chain interfaces.
Key Takeaways:
- Implementing a sender contract requires constructing a payload, specifying a destination, paying a fee, and calling the protocol's entry point (
EndpointorRouter). - LayerZero and Chainlink CCIP provide similar capabilities but differ in their specific APIs, such as their fee mechanisms (native currency vs.
LINKtokens) and how they structure message data. - The sender contract's primary role is to correctly format the request and provide the necessary fees, abstracting away the underlying complexity of off-chain relays and validation.
Preview of the Next Lesson:
We've successfully dispatched a message into the cross-chain void. But what happens on the other side? In our next lesson, we will complete the circuit by implementing the corresponding Receiver contract. We will explore how this contract is activated by the messaging protocol and how it securely decodes and processes the incoming message from our sender.
Can't find a good explanation? Sign up and we'll make it for you
Sign up