[Part 2] How to create a cross chain Multisig using Axelar

[Part 2] How to create a cross chain Multisig using Axelar

This is second part of a two part blog series in which I will showcase how you can create a Cross Chain Multisig leveraging Axelar's General Messsage

Let's revise what we learned in last part:

  • Created a contract on a Chain-1 which will store basic details about safe .

  • Send a Cross chain message using Axelar's GMP to Chain-2 whenever a new safe is created.

  • Defined function to receive cross chain encoded payload from Chain 2, function to create safe, add funds, withdraw funds, sign/approve transaction.

What will we do in this part ?

  • In the next step, you will create a new contract deployable on a separate blockchain network, which we'll refer to as Chain 2.

  • This contract will serve as a registry to record and maintain information about the safes (identified by their unique IDs) and their associated owners.

  • These safes will be initially created and deployed on Chain 1, the primary blockchain network where the first contract resides.

  • By having this registry contract on Chain 2, you'll be able to keep track of the safes and their owners across multiple blockchain networks, facilitating cross-chain interoperability and ensuring data consistency.

Step 1 : Import Axelar's libraries from it's SDK to use functions defined in their SDK.

  • Up next, we are creating a contract and extending it to support "AxelarExetuable" so that we can use it's functions.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';

contract secondContract is AxelarExecutable { 

}

Step 2 : Create a constructor which will initialise the gateway and gas receiver contract addresses.

  • Every chain has a different Gateway and Gas receiver which are used to send messages interchain.

  • You can take gateway as onchain Postman whom we pay gas to deliver our message.

  • You can find list of all the Axelar supported chains and their Gateway and gas receiver address here :

    IAxelarGasService public immutable gasService;  

    constructor(address _gateway, address _gasReceiver) AxelarExecutable(_gateway) {
        gasService = IAxelarGasService(_gasReceiver);
    }
  • Here we will add the gateway and gas receiver address of the chain on which second contract resides.

  • Eg : If our second chain in BNB Testnet then the gateway and gas receiver contract will be taken from the Axelar's Testnet contract addresses mentioned above.

Step 3 : Remember we sent a Cross chain message in our first contract with two parameters ,now we will write a function to receive those parameters and set those values on our current contract.

    mapping(uint256 =>bool) signingStatus;
    mapping(uint256 => address) safeOwner;
    function _execute(
        string calldata _sourceChain,
        string calldata _sourceAddress,
        bytes calldata _payload
    ) internal override {
        uint256 safeId;
        address secondOwner;
        (safeId,secondOwner) = abi.decode(_payload, (uint256,address));
        safeOwner[safeId] = secondOwner;
    }

This above code defines two mappings and an internal function named _execute.

  1. Mappings:

    • signingStatus: A mapping that associates a uint256 value (safe ID) with a boolean value. This is used to track the signing status of a safe.

    • safeOwner: A mapping that associates a uint256 value (safe ID) with an Ethereum address. This is used to store the second owner's address for each safe.

  2. _execute Function:

    • This function is marked as internal and override, because it's being defined in AxelarExecutable.sol which we are inheriting and hence to use _Execute , we need to define it as internal.

    • It takes three parameters:

      • _sourceChain: A string representing the source blockchain network.

      • _sourceAddress: A string representing the source contract address on the source chain.

      • _payload: A byte array containing encoded data.

    • Inside the function, it decodes the _payload bytes using abi.decode into two variables: safeId (a uint256) and secondOwner (an address).

    • It then stores the secondOwner address in the safeOwner mapping, associating it with the corresponding safeId .

Step 4 : We now have safe Id and and it's owner, let's create a function to set status for a particular safe and send another Cross chain Message using Axelar's GMP to the chain 1.

    function setStatus(uint256 _safeId,string calldata destinationChain,
        string calldata destinationAddress) external payable {
        require(msg.sender == safeOwner[_safeId], "Intruder spotted");
        signingStatus[_safeId] = true;

        bytes memory payload = abi.encode(_safeId,true);
        require(msg.value > 0, 'Gas payment is required');
        gasService.payNativeGasForContractCall{ value: msg.value }(
            address(this),
            destinationChain,
            destinationAddress,
            payload,
            msg.sender
        );
        gateway.callContract(destinationChain, destinationAddress, payload);
        emit sendNotification(msg.sender, _safeId);

    }

This above code defines a function named setStatus that allows the second owner of a safe to set the signing status for that safe on a different blockchain network (Chain 2).

Here's a breakdown of what the function does:

  1. It takes three parameters:

    • _safeId: A uint256 representing Unique ID of the safe.

    • destinationChain: A string representing the blockchain network where the destination contract is deployed.

    • destinationAddress: A string representing the address of the destination contract on the destinationChain.

  2. It first checks if the caller of the function (msg.sender) is the second owner of the safe identified by _safeId. If not, it reverts with the error message "Intruder spotted".

    1. This second owner is set using GMP everytime a new safe is created on Chain 1.
  3. If the caller is the second owner, it sets the signingStatus[_safeId] to true, indicating that the second owner has signed for the safe.

  4. It then encodes the _safeId and the boolean value true into a payload using abi.encode.

  5. It requires that the transaction includes some Ether/native gas token (msg.value > 0) to pay for gas on the destination chain and for successful completion of GMP.

  6. You can read more on how to estimate the gas required for call on Axelar Gas Estimation Docs .

  7. It calls the payNativeGasForContractCall function on a contract instance named gasService, passing the current contract's address, destinationChain, destinationAddress, payload, and msg.sender as arguments. This function call is preceded by sending the msg.value amount of Ether/native gas token.

  8. It calls the callContract function on a contract instance named gateway, passing destinationChain, destinationAddress, and payload as arguments. This triggers an interaction with a contract on the destination chain and passed the encoded payload using GMP.

  9. Finally, it emits an event called sendNotification with the msg.sender address and _safeId.

Step 5 : Create Getter function to get signing status of a safe by it's SafeId.

    function getStatus(uint256 _safeId) public view returns (bool) {
        return signingStatus[_safeId];
    }

Step 6 : Create a function to get Owner of a safe by SafeId

     function getOwner(uint256 _safeId)
        public view
        returns (address _owner2, uint256 _safeIdd)
    {
        return (safeOwner[_safeId],_safeId);
    }

If you have any doubts ,feel free to reach out to me on Telegram

Full code of this contract can be found here : Part 2 Cross chain Multisig contract .