[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
.
Mappings:
signingStatus
: A mapping that associates auint256
value (safe ID) with a boolean value. This is used to track the signing status of a safe.safeOwner
: A mapping that associates auint256
value (safe ID) with an Ethereum address. This is used to store the second owner's address for each safe.
_execute
Function:This function is marked as
internal
andoverride
, 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 usingabi.decode
into two variables:safeId
(auint256
) andsecondOwner
(anaddress
).It then stores the
secondOwner
address in thesafeOwner
mapping, associating it with the correspondingsafeId
.
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:
It takes three parameters:
_safeId
: Auint256
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 thedestinationChain
.
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".- This second owner is set using GMP everytime a new safe is created on Chain 1.
If the caller is the second owner, it sets the
signingStatus[_safeId]
totrue
, indicating that the second owner has signed for the safe.It then encodes the
_safeId
and the boolean valuetrue
into a payload usingabi.encode
.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.You can read more on how to estimate the gas required for call on Axelar Gas Estimation Docs .
It calls the
payNativeGasForContractCall
function on a contract instance namedgasService
, passing the current contract's address,destinationChain
,destinationAddress
,payload
, andmsg.sender
as arguments. This function call is preceded by sending themsg.value
amount of Ether/native gas token.It calls the
callContract
function on a contract instance namedgateway
, passingdestinationChain
,destinationAddress
, andpayload
as arguments. This triggers an interaction with a contract on the destination chain and passed the encoded payload using GMP.Finally, it emits an event called
sendNotification
with themsg.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 .