Cross-chain
Security
Blog posts
go back

The cross-chain status quo

Writing cross-chain smart contracts

January 12, 2023
tags
Cross-chain
Security
Blog posts
Writing, testing, and deploying an application that bridges multiple blockchains is tough, but this three-part blog series aims to make it easier. We walk through the cross-chain development status quo---from building (Part I) to testing (Part II) to deployment and monitoring (Part III)---with code examples for multiple chains and bridge providers. This post, the first installment, covers _writing cross-chain smart contracts_. <br> <br> ## What is a cross-chain application?<br> <br> A cross-chain application sends some _state_ (e.g., tokens, numbers, etc) from a source chain to a destination chain. For example, consider the [UniSwap protocol](https://uniswap.org). UniSwap includes contracts on many different chains that users can invoke to send assets between one supported chain and another. <br> <br> ## Writing smart contracts that communicate cross-chain<br> <br> Writing cross-chain smart contracts is a fundamentally different beast than writing, say, a single smart contract for deployment on multiple chains. In the cross-chain case, there's a *sender contract* on chain `A` that sends messages to a *receiver contract* on chain `B`. The messages are relayed from chain `A` to chain `B` using some kind of *relayer service*, or *bridge* (discussed next). If you use an off-the-shelf relayer, writing sender and receiver contracts is (usually) a matter of implementing sender and receiver interfaces provided by the relayer you've selected. We'll make this more concrete with some code examples right after discussing cross-chain bridge provider options.<br> <br> > Cubist's SDK makes cross-chain development easy by _automatically<br> > handling_ the details of cross-chain interactions. This makes building<br> > cross-chain dapps easier and safer, and it means you can change bridge<br> > providers with one line of configuration.<br> > [Try the Cubist SDK here!][sdkgithub]<br> <br> ## Cross-chain bridge solutions<br> <br> Rather than building your own bridge from scratch, it's almost certainly easier (and safer!) to use an existing bridging solution. Here are some well-known cross-chain bridge providers to consider:<br> <br> - [Axelar](https://axelar.network)<br> - [LayerZero](https://layerzero.network)<br> - [Polkadot](https://polkadot.network/cross-chain-communication/)<br> - [CCIP](https://chain.link/cross-chain) is not yet public. <br> <br> Different bridges have different costs and security properties; this blog post _doesn't_ outline the pros and cons of each provider. Before deciding on a provider, it's worth reading the documentation for the bridges you're considering, trying the examples in their documentation, etc. <br> <br> Typically, cross-chain bridge providers have a messaging API that developers can use by implementing sender contract interfaces and receiver contract interfaces. To send and receive data using Axelar, for example, smart contracts implement the `AxelarExecutable` interface. In the next sections, we walk through how sender and receiver interfaces work, and give concrete examples of each using the Axelar and LayerZero bridge services.<br> <br> ## Example overview: making a simple storage contract cross-chain<br> <br> Let's say we want to build an Avalanche smart contract that stores a number both locally and on Ethereum; any time `store(number)` is called on Avalanche, that number will also eventually be stored on Ethereum. <br> <br> Here's the Ethereum storage code:<br> <br> ```solidity<br> contract EthStorage {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;uint256 _stored;<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function store(uint256 num) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = num;<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> This smart contract's `store()` function takes a number as input and stores that number in the `stored` field of the contract.<br> <br> Here's the Avalanche code we _would like_ to write:<br> <br> ```solidity<br> import 'EthStorage.sol';<br> <br> contract AvaStorage {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;uint256 _stored;<br> &nbsp;&nbsp;&nbsp;&nbsp;EthStorage _ethStorage;<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;constructor(EthStorage ethStorage) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_ethStorage = ethStorage;<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = 0;<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function store(uint256 num) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = num;<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_ethStorage.store(num);<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> This smart contract's `store` function takes a number as input and (1) stores it in `AvaStorage`'s `stored` field and (2) calls `EthStorage`'s `store()` with it.<br> <br> The `AvaStorage` and `EthStorage` smart contracts would both work fine if they were deployed on the same blockchain---`AvaStorage` would call `EthStorage`'s `store()`, and `num` would get stored in both contracts. Unfortunately, `AvaStorage` and `EthStorage` are _on different chains_, so they can't interact directly; they'll have to interact using a cross-chain bridge provider. To use an existing bridge provider, you typically have to implement a _sender contract_ (in this case, on Avalanche) and a _receiver contract_ (in this case, on Ethereum). We'll turn `AvaStorage` into a sender contract and `EthStorage` into a receiver contract in the next two sections. <br> <br> > Cubist's SDK _automatically converts_ smart contracts just like the ones<br> > above into cross-chain contracts that are _bridge-provider agnostic_.<br> > In other words, if you were using Cubist's SDK, you'd already be done<br> > creating your cross-chain dapp! [Start building with the Cubist SDK!][sdkgithub]<br> <br> ## The sender contract, `AvaStorageSender`<br> <br> Conceptually, here's how our sender contract on Avalanche will work:<br> ```solidity<br> contract AvaStorageSender is CrossChainSender {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;uint256 _stored;<br> &nbsp;&nbsp;&nbsp;&nbsp;EthStorage _ethStorage;<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;// assume constructor initializes members in the obvious way<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function store(uint256 num) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = num;<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// both of next two methods are defined in the parent<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// `CrossChainSender` contract<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;payForCrossChainCall();<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;crossChainCall(_ethStorage, encode(num));<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> This is _not executable code_, but it gives you a sense of how the sender contract operates. It will:<br> <br> 1. Implement a bridge provider--dependent interface for sending (represented by `CrossChainSender` in this example)<br> <br> 2. Pay the bridge provider for a forthcoming cross-chain call (represented by `store()`'s invocation of `payForCrossChainCall()` in this example)<br> <br> 3. Encode the payload (`encode()` in this example) as `bytes` and then call the cross-chain smart contract through the bridge provider (represented by `crossChainCall()` in this example).<br> <br> Next, let's look at what our receiver contract will look like.<br> <br> ## The receiver contract, `EthStorageReceiver`<br> <br> Here's the logic of `EthStorageReceiver` deployed on Ethereum:<br> <br> ```solidity<br> contract EthStorageReceiver is CrossChainReceiver {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;uint256 _stored;<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function receiveCrossChainCall(bytes calldata payload) override {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = decode(payload, (uint256)); <br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> One again, this is _not_ executable code---it just shows the logic that `EthStorageReceiver` should implement. It will:<br> <br> 1. Implement a bridge provider--dependent interface for receiving (represented by `CrossChainReceiver` in this example).<br> 2. Implement a function that the bridge provider triggers whenever `AvaStorageSender` makes a cross-chain call (`receiveCrossChainCall()` in this example). Note that there is _only one_ receiving method for most cross-chain bridge interfaces (e.g., Axelar and LayerZero). If the receiver contract contains multiple functions and wants to expose more than one cross-chain, the logic _inside_ `receiveCrossChainCall()` will have to dispatch to the correct function. We don't discuss this in depth here, but our [more complex examples](https://github.com/cubist-dev/axelar-CounterAvaEth-example) show one solution for making multiple receiver functions work. In this example, all `receiveCrossChainCall()` has to do is update `stored` to correctly reflect the value sent by `AvaStorageSender`, so all that needs to be encoded in `payload` argument is that value; `EthStorageReceiver` simply decodes `payload` using the `decode()` function, then updates `_stored`.<br> <br> One important consideration that this example does not handle is ensuring that only authorized callers can execute `receiveCrossChainCall`. We discuss this issue more below.<br> <br> ## Axelar sender and receiver<br> <br> In this section, we implement sender and receiver contracts using [Axelar](https://docs.axelar.dev) as our cross-chain bridge provider.<br> <br> ### The Axelar `AvaStorageSender`<br> <br> Here's the sender contract on Avalanche using Axelar as its bridge provider:<br> <br> ```solidity<br> import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executables/AxelarExecutable.sol";<br> import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";<br> import {IAxelarGasService} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol";<br> <br> contract AvaStorageSender is AxelarExecutable { // on Avalanche<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;IAxelarGasService public immutable _gasReceiver;<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;// The destination of the receiver contract, stored as a string<br> &nbsp;&nbsp;&nbsp;&nbsp;// to accommodate different address formats on foreign chains.<br> &nbsp;&nbsp;&nbsp;&nbsp;string _ethCounterReceiverAddress;<br> &nbsp;&nbsp;&nbsp;&nbsp;uint256 _stored;<br> &nbsp;&nbsp;&nbsp;&nbsp;<br> &nbsp;&nbsp;&nbsp;&nbsp;constructor(<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;address gateway,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;address gasReceiver,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string memory ethCounterReceiverAddress<br> &nbsp;&nbsp;&nbsp;&nbsp;) AxelarExecutable(gateway) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// the following members are<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_gasReceiver = IAxelarGasService(gasReceiver);<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_ethCounterReceiverAddress = ethCounterReceiverAddress;<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function store(uint256 num) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = num;<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;payload = abi.encode(num);<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_gasReceiver.payNativeGasForContractCall{value: msg.value}(<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;address(this),<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"ethereum",<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_ethCounterInterfaceAddress,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;payload,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.sender<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;);<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;gateway.callContract("ethereum", _ethCounterReceiverAddress, payload);<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> This version is a lot more complex than the simple sender example from last section, but it follows the same basic structure: the smart contract implements the `AxelarExecutable` interface, uses `gateway.callContract` to make the cross-chain call, and pays for that call with the `payNativeGasForContractCall` function. To understand how this sender contract works, let's work backward from that cross-chain `callContract` invocation.<br> <br> #### Cross-chain calling: `callContract()`<br> <br> The AvaStorage contract invokes `callContract()` ([docs here](https://docs.axelar.dev/dev/build/gmp-messages)) with three arguments:<br> <br> 1. `"ethereum"`, the name of the destination chain. Axelar represents different supported chains using strings. See the strings representing Axelar's [supported testnets](https://docs.axelar.dev/dev/build/chain-names/testnet) and [supported mainnets](https://docs.axelar.dev/dev/build/chain-names/mainnet).<br> <br> 2. `_ethCounterReceiverAddress`, the address of the receiver contract on Ethereum. We haven't shown the code for this smart contract yet; it appears in the next section. This argument is a `string` because addresses don't necessarily take the same form from one chain to another (e.g., EVM addresses are 20 bytes, but not all blockchains use 20 byte addresses). <br> <br> 3. `payload`, the actual (encoded) data that we're sending to Ethereum. In this case, the payload is `num`, the number we just stored as `_stored` on Avalanche and that we'll be storing on Ethereum as well. _The payload has type `bytes` and must be encoded into bytes_ using Solidity's ABI encoding functionality (e.g., [`abi.encode()`](https://docs.soliditylang.org/en/v0.8.0/units-and-global-variables.html?highlight=abi.encode#abi-encoding-and-decoding-functions)). <br> <br> In addition to these three arguments---the destination chain, the destination address, and the payload---`callContract()` is also called on the `gateway` object. `gateway` is the [Axelar gateway contract address](https://docs.axelar.dev/learn#gateway-smart-contracts). There's one Gateway contract deployed on each chain Axelar supports, and the Gateway is what passes messages to/from the Axelar network to the chain. In other words, once the sender contract calls `callContract()` on `gateway`, the Gateway contract passes the payload and other information to the Axelar network, where it is ferried to the destination chain. See Axelar's blog for a [discussion of its network and security properties](https://axelar.network/blog/a-technical-introduction-to-the-axelar-network); for more information, see the [Axelar whitepaper](https://axelar.network/axelar_whitepaper.pdf).<br> <br> Finally, **it's also possible to call a smart contract on another chain _and_ send tokens along with the call.** See [the Axelar docs](https://docs.axelar.dev/dev/build/gmp-tokens-with-messages) for more information. `callContractWithToken()` takes two extra arguments after the payload:<br> <br> 1. (argument 4) `string symbol`, the Axelar representation of the token type you'll be sending. Right now, the two options are both USDC: symbol `"USDC"` on Ethereum, and symbol `"axlUSDC"` (Axelar Wrapped USDC) on Avalanche, BNB, Phantom, Moonbean, and Polygon. USDC is native to Ethereum, so USDC tokens must be [wrapped](https://learn.robinhood.com/articles/what-are-wrapped-tokens/) on other blockchains. For more information, refer to the "Symbol" column on the left in the [Axelar documentation](https://docs.axelar.dev/dev/build/contract-addresses/mainnet#assets).<br> <br> 2. (argument 5) `uint256 amount`. This is the number of tokens to transfer with the call; using `1` would correspond to sending 0.000001 USDC cross-chain ([USDC has 6 decimals](https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48)).<br> <br> #### Paying: `payNativeGasForContractCall()`<br> <br> Before invoking `callContract()`, the `store()` function must pay the gas fees associated with the cross-chain call by using `payNativeGasForContractCall()` (see [Axelar's docs](https://docs.axelar.dev/dev/gas-services/pay-gas#paynativegasforcontractcall)). This function signature is:<br> <br> ```solidity<br> function payNativeGasForContractCall(<br> &nbsp;&nbsp;&nbsp;&nbsp;address sender,<br> &nbsp;&nbsp;&nbsp;&nbsp;string calldata destinationChain,<br> &nbsp;&nbsp;&nbsp;&nbsp;string calldata destinationAddress,<br> &nbsp;&nbsp;&nbsp;&nbsp;bytes calldata payload,<br> &nbsp;&nbsp;&nbsp;&nbsp;address refundAddress<br> ) external payable;<br> ```<br> <br> In more detail, its arguments are:<br> <br> 1. The `sender` address (in this case, `address(this)`) indicates the sender whose gas we are pre-paying. Suppose `AvaStorageSender` was pre-paying for `OtherSender` to send the payload cross-chain; in that case, the `sender` address would be `OtherSender`'s address.<br> 2. The destination chain's string representation (see Axelar's [testnet chain strings](https://docs.axelar.dev/dev/build/chain-names/testnet)).<br> 3. The destination address for the receiver contract. Recall that this argument is a `string` for cross-chain compatibility.<br> 4. The payload. This is the same payload we used in `callContract()`.<br> 5. Address of the party receiving any refunds for gas overpayment. Note that this address _does not_ have to be the same as the sender (or pay-er) address.<br> <br> Finally, see that we've sent `{value: msg.value}`. This value [specifies how much money to send](https://docs.soliditylang.org/en/v0.8.17/control-structures.html#external-function-calls) to the receiving contract---in this case, the same amount that was sent to `AvaStorageSender`. <br> <br> Note that `payNativeGasForContractCall()` is called on the `_gasReceiver`, an instance of an Axelar gas service. The [Gas Receiver](https://docs.axelar.dev/dev/gas-services/intro#gas-receiver) is a smart contract deployed on each supported blockchain. It provides different methods for pre-paying for cross-contract calls (i.e., in the words of the docs, "paying...the relayer gas fee upfront on the source chain, thereby covering the cost of gas to execute the final transaction on the destination chain"). The Gas Receiver supports [a number of different payment options](https://docs.axelar.dev/dev/gas-services/pay-gas#alternative-gas-payment-methods-for-callcontract).<br> <br> #### Encoding the payload<br> <br> As we move upwards in `AvaStorageSender` to the line "`payload = abi.encode(num)`", recall that the payload must be encoded in bytes in order to send it across the network. See the documentation on [Solidity's ABI encoding](https://docs.soliditylang.org/en/v0.8.17/units-and-global-variables.html#abi-encoding-and-decoding-functions) for details.<br> <br> ### Axelar `EthStorageReceiver`<br> <br> Here's what the `EtherStorageReceiver` Ethereum contract looks like using Axelar:<br> <br> ```solidity<br> import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executables/AxelarExecutable.sol";<br> import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";<br> import {IAxelarGasService} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol";<br> <br> contract EthStorageReceiver is AxelarExecutable {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;IAxelarGasService public immutable _gasReceiver;<br> &nbsp;&nbsp;&nbsp;&nbsp;uint256 _stored;<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;constructor(<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;address gateway,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;address gasReceiver,<br> &nbsp;&nbsp;&nbsp;&nbsp;) AxelarExecutable(gateway) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_gasReceiver = IAxelarGasService(gasReceiver);<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function _execute(<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string calldata, // source chain (not used here)<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string calldata, // source address (not used here)<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bytes calldata payload<br> &nbsp;&nbsp;&nbsp;&nbsp;) internal override {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = abi.decode(payload, (uint256)); <br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> The key function that this smart contract must implement is `_execute()`: that's the function that Axelar invokes when a smart contract on another blockchain has made a call to `EthStorageReceiver`. `_execute()` takes the following parameters (see also the [Axelar documentation](https://docs.axelar.dev/dev/build/gmp-messages)):<br> <br> 1. The source chain. This tells `EthStorageReceiver` where the message it just received is coming from (once again [in `string` form](https://docs.axelar.dev/dev/build/chain-names/testnet)).<br> 2. The source contract address. This also a `string` for cross-chain compatibility.<br> 3. The payload, `payload`. Once again note that `payload` is encoded in bytes, so must be decoded in order to be used.<br> <br> Before calling `_execute()`, `AxelarExecutable` ensures that the message was indeed sent by the `_gateway`. The receiver contract is responsible for ensuring that the caller (identified by the first two arguments to `_execute()`) is authorized.<br> <br> Receiver contracts are a bit less complicated than sender contracts, since (by convention) payment tends to happen on the sender side. See [our runnable example](https://github.com/cubist-dev/axelar-CounterAvaEth-example) for more sophisticated payloads: this example dispatches to different functions in the receiver contract from within `_execute()`. <br> <br> ## LayerZero sender and receiver<br> <br> In this section, we see what `AvaStorageSender` and `EthStorageReceiver` look like using the [LayerZero](https://layerzero.gitbook.io/docs/) cross-chain bridge provider. <br> <br> ### LayerZero `AvaStorageSender`<br> <br> Here's a LayerZero implementation of a sender contract:<br> <br> ```solidity<br> contract AvaStorageSender is LzApp {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;constructor(address lzEndpoint) LzApp(lzEndpoint) {}<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function store(unit256 num) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bytes memory payload = abi.encode(num);<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_lzSend(<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;10121,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;payload,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.sender,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0x0,<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bytes("")<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;);<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> LayerZero's sender contract is similar to the senders in previous sections: the smart contract implements the [`LzApp` interface](https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/lzApp/LzApp.sol), and uses `_lzSend` to both make the cross-chain call and pay for that call.<br> <br> To make a cross-chain call, `AvaStorageSender` invokes `_lzSend()` with six arguments (see [LayerZero's docs](https://layerzero.gitbook.io/docs/evm-guides/master/how-to-send-a-message). Note that this doc starts with raw `send()`, which we describe [later](#goingwithouttheinterface); let's focus on `_lzSend()` for now):<br> <br> 1. The `uint16` chain ID of the destination chain (in this case, for Ethereum).<br> 2. The `bytes payload`, the actual (encoded) data that the sender contract is sending to Ethereum. In this case, the payload is `num`, the number we just stored as `stored` on Avalanche and that we'll be storing on Ethereum as well; we encode `num` using the same Solidity encoding functionality we used for Axelar.<br> 3. The `address` refund address. If we've overpaid gas for this cross-chain call, the excess will be refunded to `address`, much as it is in Axelar.<br> 4. The `address` that will pay `ZRO` tokens for the call. Note that at the time of this writing, ZRO tokens are not yet available.<br> 5. The `bytes adapterParams`. Adapter parameters are supposed to let you customize the amount of gas you send with a cross-chain transaction.<br> <br> The [LayerZero docs on adapter parameters](https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters) aren't complete as of January 2023, so we'd recommend avoiding them for now.<br> <br> ### LayerZero `EthStorageReceiver`<br> <br> Here's what the `EtherCounterReceiver` Ethereum contract looks like using LayerZero:<br> <br> ```solidity<br> import {LzApp} from "@layerzerolabs/solidity-examples/contracts/lzApp/LzApp.sol";<br> <br> contract EthStorageReceiver is LzApp {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;uint256 _stored;<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;constructor(address lzEndpoint) LzApp(lzEndpoint) {}<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function _blockingLzReceive(uint16, bytes calldata, uint64, bytes calldata payload) override external {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = abi.decode(payload, (uint256));<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> The key function that this smart contract must implement is `_blockingLzReceive()` (see [LayerZero's docs](https://layerzero.gitbook.io/docs/evm-guides/master/receive-messages)). `_blockingLzReceive()` takes the same arguments as `lzReceive()`; we explain the difference [later on](#blockinginlayerzero) in this blog post). LayerZero invokes `_blockingLzReceive()` when a smart contract on another blockchain has made a call to `EthStorageReceiver`. Before calling `_blockingLzReceive()`, the `LzApp` smart contract checks that the message sender is a LayerZero endpoint, and it checks that the source of the cross-chain message is a trusted contract. Since we emitted a `SetTrustedRemoteAddress` event from `AvaStorageSender`'s constructor, `_lzSend()` calls from `AvaStorageSender` are allowed to connect via the LayerZero network with the `_blockingLzReceive()` function on `EthStorageReceiver`. <br> <br> `_blockingLzReceive` takes the following parameters:<br> <br> 1. The `uint16` source chain ID (not used here), which tells `EthStorageReceiver` where the message it just received is coming from. <br> 2. The `bytes` source contract address (not used here), which tells `EthStorageReceiver` the originating address of the call. This parameter is _not_ just the sender contract address encoded in bytes; rather, it's the sender "path" encoded in the same manner we encoded the argument to `SetTrustedRemoteAddress()`: the address of the sender and receiver contracts concatenated and encoded, [as described in the LayerZero docs](https://github.com/LayerZero-Labs/set-trusted-remotes#approach-2-manual). <br> 3. The `uint64` nonce of the message. Note that this is provided by LayerZero; in other words, `_lzSend()` on the source chain does not send a nonce value explicitly. The nonce is an ordered value that your application can use to determine the order in which messages were sent. <br> 4. The payload, `payload`. Once again note that `payload` is encoded in bytes, so must be decoded in order to be used.<br> <br> <br> #### Securing cross-chain calls<br> <br> In LayerZero, cross-chain callers must be authorized before they can invoke `LzApp` receiver functions. This is handled by [setting a trusted remote](https://layerzero.gitbook.io/docs/evm-guides/master/set-trusted-remotes); see also the [example code](https://github.com/LayerZero-Labs/set-trusted-remotes) provided by LayerZero.<br> <br> #### Going without the interface<br> <br> Unlike Axlear, LayerZero allows you to use the bridge service _without_ implementing a cross-chain interface; instead, your smart contract can store a reference to an `ILayerZeroEndpoint` that the contract can use to send messages cross-chain. There is a single LayerZero endpoint for each blockchain that LayerZero supports. Here's how `AvaStorageSender` might use the `ILayerZeroEndpoint` directly:<br> <br> ```solidity<br> import {ILayerZeroEndpoint} from "@layerzerolabs/solidity-examples/contracts/interfaces/ILayerZeroEndpoint.sol";<br> <br> contract AvaStorageSender {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;ILayerZeroEndpoint public immutable _lzEndpoint;<br> &nbsp;&nbsp;&nbsp;&nbsp;<br> &nbsp;&nbsp;&nbsp;&nbsp;// ...<br> &nbsp;&nbsp;&nbsp;&nbsp;<br> &nbsp;&nbsp;&nbsp;&nbsp;function store(uint256 num) {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// ...<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_lzEndpoint.send(...);<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> Despite the fact that using the `LzEndpoint` directly is _possible_, we recommend using LayerZero's `LzApp` interface instead. **The `LzApp` interface automatically checks that messages came from a LayerZero endpoint and, further, that they originated from a trusted contract on the source chain.** You almost certainly want to use an interface for security reasons; using the `lzEndpoint` directly requires re-implementing some `LzApp` functionality (e.g., trusted sender checks on the receiving side), which takes development time and risks security problems down the road.<br> <br> #### Blocking in LayerZero<br> <br> By default, if the delivery of a cross-chain message fails, it blocks the delivery channel until the delivery succeeds (i.e., during this time no other messages will be delivered through this channel). This enforces message ordering by nonce. For dapps that require failed messages to be non-blocking, LayerZero provides an abstract contract, `NonblockingLzApp`, that stores failed messages on the target chain for later retry.<br> <br> A non-blocking version of the previous smart contract looks like this:<br> <br> ```solidity<br> import {NonblockingLzApp} from "@layerzerolabs/solidity-examples/contracts/lzApp/LzApp.sol";<br> <br> contract EthStorageReceiver is NonblockingLzApp {<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;uint256 _stored;<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;constructor(address lzEndpoint) NonBlockingLzApp(lzEndpoint) {}<br> <br> &nbsp;&nbsp;&nbsp;&nbsp;function _nonblockingLzReceive(uint16, bytes memory, uint64, bytes calldata payload) override external {<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_stored = abi.decode(payload, (uint256));<br> &nbsp;&nbsp;&nbsp;&nbsp;}<br> }<br> ```<br> <br> As this example shows, only two changes are required to make our receiver non-blocking:<br> 1. Instead of inheriting from the abstract contract `LzApp`, we inherit from `NonblockingLzApp`.<br> 2. The method to override for receiving messages is `_nonblockingLzReceive()` instead of `_blockingLzReceive()`.<br> <br> Failed messages can be retried by calling `retryMessage()` on the smart contract. The abstract `NonblockingLzApp` contract also defines two events, `MessageFailed` and `RetryMessageSuccess` that indicate when a given message delivery has failed or a retry has succeeded, respectively.<br> <br> ## Try it yourself!<br> <br> We've created a slightly more complex [executable Axelar example](https://github.com/cubist-dev/axelar-CounterAvaEth-example) with instructions. In Part II of this blog series, we will cover the cross-chain testing status quo, and in Part III, the cross-chain deployment status quo. **Stay tuned for more!**<br> <br> > The Cubist SDK makes cross-chain development easy by _automatically<br> > generating_ the bridge code that lets your smart contracts interact. As a result<br> > of this auto-generation, Cubist lets you change cross-chain bridge providers with<br> > a single line of configuration. [Get started with the Cubist SDK!][sdkgithub]<br> <br> [sdkgithub]: https://github.com/cubist-labs/<br> <br>

Read more

Why we're joining the Shared Security Alliance

Why we're joining the Shared Security Alliance

We look forward to bringing our expertise in anti-slashing to the Shared Security Alliance and collaborating with our fellow members to promote safe practices in the restaking community.

June 18, 2024
What's embedded in your embedded wallet?

What's embedded in your embedded wallet?

Here are the four questions to ask before choosing your embedded wallet provider. If you want to keep your users’ keys safe—and keep yourself safe from key custody risk—read on.

May 6, 2024
Cubist joins the Allora Network as a node operator

Cubist joins the Allora Network as a node operator

As a node operator, Cubist is supporting Allora’s mission by operating a validator to secure the Allora chain and a Reputer to rate the performance of the ML models delivered by Allora Workers.

April 15, 2024