Communication between multiple chains has become more and more important. Although the major security responsibility and trust are on the selected bridge protocol, bad cross-chain app implementation on top of these bridges can lead to catastrophic consequences. This guide covers the main threats, mistakes, and best practices for developing secure cross-chain apps on the Axelar protocol.

Architecture

We will focus mainly on the application layer and gateway, but it is good to have basic knowledge of the whole architecture.

Axelar has deployed AxelarGateway contracts on every chain, which acts as a middle layer between applications and Axelar network and provides communication between these layers. On the source chain, the gateway sends messages (as events) to validators and eventually burns/locks tokens. On the destination chain, the gateway is responsible for received message validations and eventually minting/unlocking tokens. Axelar network consensus is achieved by a set of validators using delegated proof-of-stake.

General Message Passing protocol

Axelar GMP is the fundamental feature of Axelar network, which provides developers an easy way to send tokens and call functions across supported chains. What does the message flow look like?

  1. Application on the source chain calls one of these gateway functions:
    – sendTokento send only tokens,
    – callContractto execute apayload on the destination chain,
    – callContractWithToken to send tokens and execute a call.
  2. Gateway emits an event with all parameters.
  3. Validators validate the message and notify the destination gateway.
  4. The execute function is called in the app on the destination chain.

GMP Express

Privileged clients (protocols) can be allowed by Axelar to use the Express version of the messaging protocol. A standard message can take several minutes for the Axelar network to fully approve the message and pass it to the final destination. The express message will still go through the Axelar network, but GMP Express Service will lend any sent tokens to the destination address while approval is happening. Once the complete approval is done, tokens are paid back to the GMP Express service. This process can speed up the cross-chain transfer more than ten times. The crucial part of GMP Express is complete trust between the privileged protocol and Axelar. The privileged protocol MUST implement the logic for token returns when a standard message is received. Otherwise, the privileged protocol will receive 2x more tokens. See ExpressExecutable for more information.

How to start

Developing apps on Axelar is simple and straightforward, using Axelar GMP SDK. Just add @axelar-network/axelar-gmp-sdk-solidity dependency to your Solidity project, and you are good to go.

Application is AxelarExecutable

@axelar-network/axelar-gmp-sdk-solidity/contracts/executables/AxelarExecutable.sol

Every application built on Axelar Network has to inherit from AxelarExecutable contract, which implements IAxelarExecutable interface and handles received messages.

Receiving messages

The following internal functions are intended to be overridden in the application to handle received messages or/and tokens. This is also a critical part of application security. Pay maximum attention to avoid any reentrancies, logic bugs, or miscalculations.

If the source chain calls sendToken or callContractWithToken function on the gateway, then in the app on the destination chain _executeWithToken function gets called. Minting/unlocking tokens is handled automatically by gateway.validateContractCallAndMint function.

Sending messages

For sending messages, there is IAxelarGateway the interface of Axelar gateway, which is used to send tokens and messages.

@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol

Function sendToken provides multichain token transfer. The destination chain is identified by string id, the destination address is also a string. Depending on the type of token (internal/external), it gets burned/locked on the source chain and minted/unlocked on the destination chain.

For sending the cross-chain message (payload), use callContractfunction.

And if you need to send both (message and tokens), there is a callContractWithToken function.

How to pay gas?

There are two ways to pay gas for cross-chain calls. The preferred one is AxelarGasService contract, which works as a prepay on the source chain for transactions on the destination chain.

  1. Using AxelarJS SDK call estimateGasFee function on the destination chain to calculate the gas fee.
  2. Pass the calculated gas fee as msg.value from the smart contract to AxelarGasService contract on the source chain using one of these functions:
    – payGasForContractCall
    – payGasForContractCallWithToken
    – payNativeGasForContractCall
    – payNativeGasForContractCallWithToken

The user/app on the destination chain can also pay gas fees manually. For detailed information about paying gas fees, see this article in Axelar documentation.

Example cross-chain app

We’ve prepared a sample cross-chain app for sending messages and tokens through the Axelar network here on GitHub. The project also includes Wake tests.

Upgradability

For upgradable contracts, it’s recommended to inherit from Axelar’s Upgradable contract, which implements IUpgradable interface.

@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradables/Upgradable.sol

Then define a unique contractId constant, which needs to stay the same across the contract versions. The contractId is a hash of app/component name e.g. keccak256("your-app"). During every upgrade, the Upgradable implementation logic checks if the contractId of the new implementation matches the contractId of the Proxy. If not, the upgrade fails.

We implemented a detector for Wake static analyzer, which checks contractId constant. It warns if a proxy for implementation is missing or if more than one proxy contract with the same contractId exists.

Things to be aware of

Besides the awareness of common vulnerabilities of Solidity/EVM, cross-chain app development brings more topics to be cautious about. Let’s dive deeper into the six most important ones.

Inherit from AxelarExecutable

AxelarExecutable contract contains important checks against the Axelar Gateway to validate contract calls. Bypassing these checks (e.g. implementing IAxelarExecutable interface directly) could lead to critical vulnerabilities. Since the execute function is not protected by any modifier, everyone would be able to call it with any payload and execute any function on the destination chain.

So, ensure that your execute function validates the calls using this gateway function validateContractCall, which checks that Axelar validators confirmed the message’s source and payload are valid.

A similar approach applies to executeWithToken function, but using validateContractCallAndMint.

Axelar components’ addresses

Axelar gateway is a fundamental part of the protocol security, so it’s very important to double-check the gateway address passed to the app during the deployment. A malicious gateway would be able to manipulate message checks. This is also important from the user’s perspective. Always check that the application uses the official Axelar gateway and Gas Service addresses.

Also, the application should not contain a gateway address setter. This would decrease the trust in the application. If the owner can control the gateway address, he also would be able to manipulate cross-chain messages in many ways.

Destination chain and address validations

Before sending the message to another chain, do data validations of destinationChain and destinationAddress. Sending tokens to a non-existent chain or address means a loss of funds.

Validate the source address on the destination chain

It is important to avoid receiving messages from any source address. For this purpose, deploying the app to the same address on multiple chains is a good practice. Axelar provides for this purpose utils ConstAddressDeployer and Create3Deployer. Then, you can simply validate the source address in your _execute and _executeWithToken functions using this condition.

Express GMP data validations

If you decide to use ExpressExecutable contract, be extra careful of the receiving messages data validations. Functions execute and executeWithToken are external and aren’t protected by any modifier, so anyone can call them and potentially exploit the contract in many ways using malicious payload. Message validations using the Axelar gateway are also missing in the ExpressExecutable. Keep this in mind and implement robust data validations here.

NFT doubling

When implementing NFT cross-chain transfers, ensure the user’s NFT is properly locked in the contract and the user cannot own it on both chains simultaneously.

Static analysis, unit testing, and fuzzing

Contract development doesn’t end with programming itself. Quality assurance has to be your top priority when protocols operate with users’ funds or other valuables. There steps that development teams can and should perform internally before assigning a security audit.

Static analysis

Detecting possible vulnerabilities during the development using classic static analysis tools like Slither, MythX, Mythrill… can be helpful and also painful because of many false positive findings. Our Wake static analyzer goes deeper into this rabbit hole. Our target is to minimize false positive detections and achieve high precision while we accept a lower recall. Check out Tools For Solidity extension that shows the static analysis results directly in VSCode.

Unit testing

It is very important to test the application properly. Unit tests are useful for testing all use cases, and high test coverage is essential for basic security.

Since apps on Axelar operate on multiple chains, it brings additional complexity to the system, and cross-chain testing definitely should be part of a development pipeline. We recommend our easy-to-use testing framework Woke, for this purpose. It provides a complete set of tools for cross-chain testing and even fuzzing. See the documentation for more information. Also, don’t miss the article Testing Axelar contracts using open-source tools.

Unit and fuzz test coverage is also calculated by Wake and is displayed in the Tools For Solidity extension as well. In the case of fuzzing, the coverage is calculated realtime.

Fuzzing

Fuzzing is a technique for testing software that involves providing invalid, unexpected, or random data as inputs to a computer program.

Unit tests mostly cover the system’s intended behavior and strictly defined use cases. On the other hand, fuzz tests can test a wide range of unpredictable, random scenarios and discover even hidden vulnerabilities like inconsistent calculations.

Summary

Axelar provides developers with a powerful platform and tools to develop cross-chain applications. Axelar cross-chain applications inherit the bridge’s security, but it doesn’t mean the applications are safe. There are still potential pitfalls during the implementation of the app logic. So even if the bridge is bulletproof, it doesn’t mean that the application is and that the funds of users are safe. Always do intense internal testing and independent external security audits to achieve high-security standards and one of the most important things in Web3 — community trust.