How to develop secure cross-chain apps on Axelar
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?
- Application on the source chain calls one of these gateway functions:
-sendToken
to send only tokens,
-callContract
to execute apayload
on the destination chain,
-callContractWithToken
to send tokens and execute a call. - Gateway emits an event with all parameters.
- Validators validate the message and notify the destination gateway.
- 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.
contract YourApp is AxelarExecutable {
IAxelarGasService public immutable gasService;
constructor(
address gatewayAddress_,
address gasServiceAddress_,
) AxelarExecutable(gatewayAddress_) {
if (gatewayAddress_ == address(0)
|| gasServiceAddress_ == address(0)
) revert ZeroAddress();
gasService = IAxelarGasService(gasServiceAddress_);
}
}
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.
function _execute(
string calldata sourceChain,
string calldata sourceAddress,
bytes calldata payload
) internal virtual
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.
function _executeWithToken(
string calldata sourceChain,
string calldata sourceAddress,
bytes calldata payload,
string calldata tokenSymbol,
uint256 amount
) internal virtual
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.
function sendToken(
string calldata destinationChain,
string calldata destinationAddress,
string calldata symbol,
uint256 amount
)
For sending the cross-chain message (payload), use callContract
function.
function callContract(
string calldata destinationChain,
string calldata destinationContractAddress,
bytes calldata payload
)
And if you need to send both (message and tokens), there is a callContractWithToken
function.
function callContractWithToken(
string calldata destinationChain,
string calldata destinationContractAddress,
bytes calldata payload,
string calldata symbol,
uint256 amount
)
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.
- Using AxelarJS SDK call
estimateGasFee
function on the destination chain to calculate the gas fee. - Pass the calculated gas fee as
msg.value
from the smart contract toAxelarGasService
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 Woke 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 Woke 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.
if (!gateway.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash))
revert NotApprovedByGateway();
A similar approach applies to executeWithToken
function, but using validateContractCallAndMint
.
if (!gateway.validateContractCallAndMint(
commandId,
sourceChain,
sourceAddress,
payloadHash,
tokenSymbol,
amount
)
) revert NotApprovedByGateway();
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.
if (sourceAddress.toAddress() != address(this)) revert InvalidSourceAddress()
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 Woke 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 Woke 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.