How to create a cross chain Multisig using Axelar
Firstly let's understand what will we be building in this part
-
Deploy 2 Solidity contracts on two distinct chains which are supported by Axelar network .
Understand what is Axelar's General Message Passing(GMP).
Use General Message passing in those contracts to pass messages about various events happening on them.
Let's understand what is Axelar ?
Axelar is a decentralized network protocol that aims to connect and enable communication between different blockchain ecosystems, facilitating cross-chain interactions and composability in the Web3 space.
Some key points about Axelar :
Cross-Chain Communication: Axelar provides a secure and decentralized messaging layer that allows different blockchain networks to communicate and transfer data seamlessly. This enables interoperability between previously isolated blockchain ecosystems.
Asset Transfer: Axelar enables the cross-chain transfer of assets (tokens, NFTs, etc.) between supported blockchain networks without the need for centralized bridges or wrapped tokens. This allows for seamless asset portability across chains.
Cross-Chain Execution: Axelar facilitates cross-chain execution of smart contracts, allowing decentralized applications (dApps) to leverage the capabilities of multiple blockchain networks simultaneously.
Decentralized Ecosystem: Axelar is built as a decentralized network, with its own native token (AXL) used for staking, governance, and paying transaction fees. This promotes decentralization and censorship resistance within the Axelar ecosystem.
Supported Chains: Axelar supports interconnectivity between various blockchain networks, including Ethereum, Avalanche, Polygon, Fantom, and several others, enabling the creation of multi-chain applications and services.
What is Axelar's General Message passing ?
Axelar's General Message Passing is a key feature that enables communication and data transfer between different blockchain networks in a decentralized and secure manner.
General Message Passing works as follows:
Message Formation: A message is formed on the source blockchain network, containing data or instructions that need to be transmitted to a destination blockchain network. This message can include various types of information, such as asset transfers, cross-chain function calls, or any other data.
Message Encoding: The message is encoded into a specific format compatible with Axelar's protocol. This encoding process includes cryptographic signing and verification mechanisms to ensure the message's authenticity and integrity.
Message Propagation: The encoded message is propagated through the Axelar network, which consists of a decentralized set of validator nodes. These nodes validate and relay the message across the network, ensuring its secure delivery to the destination blockchain.
Message Delivery: Once the message reaches the destination blockchain network, it is decoded and executed according to the instructions or data contained within the message. This execution can trigger various actions, such as token transfers, smart contract function calls, or any other operations supported by the destination blockchain.
The General Message Passing feature enables various use cases and capabilities in the Web3 ecosystem, including:
Cross-Chain Asset Transfers: Tokens, NFTs, or other digital assets can be transferred seamlessly between different blockchain networks without the need for centralized bridges or wrapped tokens.
Cross-Chain Smart Contract Execution: Smart contracts on one blockchain can call and execute functions on smart contracts deployed on other blockchain networks, enabling composability and interoperability of decentralized applications (dApps).
Cross-Chain Data Exchange: Data and information can be securely shared and exchanged between different blockchain networks, enabling various cross-chain applications and services.
Axelar's General Message Passing is designed to be decentralized, secure, and censorship-resistant, aligning with the core principles of Web3 and enabling a more interconnected and collaborative blockchain ecosystem.
Shall we code some hands on smart contracts?,yes👀
Pre-requisites : Basic level of understanding in Solidity.
Step 1 : Import Axelar 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';
import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol';
contract CallContract 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;
IERC20 public token;
event Executed(string _from, string _message);
/**
*
* @param _gateway address of axl gateway on deployed chain
* @param _gasReceiver address of axl gas service on deployed chain
*/
constructor(address _gateway, address _gasReceiver) AxelarExecutable(_gateway) {
gasService = IAxelarGasService(_gasReceiver);
//0x760 is a ERC20 USDC token deployed on Avalanche Testnet
token = IERC20(0x7603946e5342d024DD4D5807769C8D5e7E6C6C21);
}
Step 3 : Create a function to create Safe on base chain and store it's information like Safe Owners,Signature Status,Balance and name.
struct addr {
string name;
address a1;
address a2;
bool sts1;
bool sts2;
uint256 balance;
uint256[] timestamps;
}
mapping(uint256 => addr) public safeOwner;
uint256 public safeId;
function createSafe(string memory _safeName, address _secondSigner,
string calldata destinationChain,
string calldata destinationAddress
)
external payable
returns (uint256)
{
safeId++;
safeOwner[safeId].name = _safeName;
safeOwner[safeId].a1 = msg.sender;
safeOwner[safeId].a2 = _secondSigner;
emit safeCreated(safeOwner[safeId].a1, safeOwner[safeId].a2, safeId);
bytes memory payload = abi.encode(safeId,_secondSigner);
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);
return (safeId);
}
- The struct
addr
is used to store basic information about the Multisig safe.
The code includes a function called createSafe
that takes the following parameters:
_safeName
(a string)_secondSigner
(an address)destinationChain
(a string)destinationAddress
(a string)
The function performs the following tasks:
Increments the
safeId
value.Assigns the
_safeName
to thename
property of thesafeOwner
mapping at thesafeId
index.Assigns the
msg.sender
address to thea1
property of thesafeOwner
mapping at thesafeId
index.Assigns the
_secondSigner
address to thea2
property of thesafeOwner
mapping at thesafeId
index.Emits an event called
safeCreated
with thea1
,a2
, andsafeId
values.Encodes the
safeId
and_secondSigner
values into a payload usingabi.encode
.Requires that
msg.value
(the amount of Ether sent with the transaction) is greater than zero.Calls a function called
payNativeGasForContractCall
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.Calls a function called
callContract
on a contract instance namedgateway
, passingdestinationChain
,destinationAddress
, andpayload
as arguments.Returns the
safeId
value.
Whenever we create a new safe, we send a cross chain message to the second contract that is being deployed on second chain,In this way the other contract will be updated on the newer safes that are being created .
You need to send some native gas tokens to the function to facilitate the contract call on the destination chain.
Now while creating the payload we are giving it two parameters but we are not specifying which function to call, hence axelar will check which function has two arguments and it will send the payload to that function ,due to this reason you can only have one function with two parameters or else the Axelar's message will not be delivered to correct function.
Further we can use gateway contract we set at the start in constructor to pass the payload to destination chain .
Step 4 : Now We have a safe,Let's add funds to it and create addFunds
.
In this function, only the Safe Owners can add funds into the safe hence by mistaken you don't add funds to some other safe which you are not owner of.
We check if the address calling the function is an owner of the safe, if yes then funds will be added or else the transaction will be reverted.
function addFunds(uint256 _safeId,uint256 amount)public{
require(safeOwner[_safeId].a1 == msg.sender || safeOwner[_safeId].a2 == msg.sender ,"You are not owner of this safe");
safeOwner[safeId].balance += amount;
token.transferFrom(msg.sender,address(this),amount);
emit addFundsEvent(safeOwner[_safeId].a1,safeOwner[_safeId].a2,_safeId,amount);
}
Step 5 : Now we have funds in safe, let's Write function to withdraw it !
To withdraw funds from safe, we need signatures of both the owners of the safe.
This function is for the Owner 1 of the safe or the person who created the safe on the Chain 1.
Emit an Event about the Signing status .
//set approval for owner 1
function SetApproval(uint256 _safeId) public {
require(
msg.sender == safeOwner[_safeId].a1 &&
safeOwner[_safeId].sts1 == false,
"You are not the owner of the safe"
);
safeOwner[_safeId].sts1 = true;
emit signedTransaction(safeOwner[_safeId].a1,safeOwner[_safeId].a2,_safeId);
}
Step 6 : Create a function to receive signing status of second signer from second chain .
This function will give us second signature which will be required to withdraw the funds.
If the second owner denies the transaction then the withdrawl will be stuck unless he accepts it.
function _execute(
string calldata _sourceChain,
string calldata _sourceAddress,
bytes calldata _payload
) internal override {
uint256 safeId;
bool status;
(safeId,status) = abi.decode(_payload, (uint256,bool));
safeOwner[safeId].sts2 = status;
if(afeOwner[safeId].sts2 == true && safeOwner[safeId].sts1 == true){
withdraw(safeId,safeOwner[safeId].balance,safeOwner[safeId].a1);
}
}
This function is automatically being called by the Axelar's relayer.
It receives the Encoded GMP message sent from the source chain.
Further,It checks if both the owners have accepted the transaction then it will call the withdraw function and transfer funds to the Owner 1 or the creator of the safe.
Step 7 : Finally , we are at the last function of this Quest, Withdraw
!
function withdraw(
uint256 _safeId,
uint256 _amt,
address payable _addr
) public {
if (
safeOwner[_safeId].sts1 == true && safeOwner[_safeId].sts2 == true
) {
require(
_amt <= safeOwner[_safeId].balance,
"The amount is greater than Safe Balance"
);
token.transfer(_addr,_amt);
emit widthrawMoney(_amt, _addr,_safeId); //eidthraw money on different chain
safeOwner[_safeId].sts1 = false;
safeOwner[_safeId].sts2 = false;
safeOwner[_safeId].balance = safeOwner[_safeId].balance - _amt;
safeOwner[_safeId].timestamps.push(block.timestamp);
}
}
The withdraw
function is a public function that allows the withdrawal of a specified amount (_amt
) from a specific "safe" (identified by _safeId
) to a given address (_addr
).
Here's a summary of what the function does:
It takes three parameters:
_safeId
(a unique identifier for the safe),_amt
(the amount to be withdrawn), and_addr
(the address to which the funds should be transferred).It checks if both
sts1
andsts2
(status flags) aretrue
for the safe identified by_safeId
.If the status flags are true, it verifies that the requested withdrawal amount (
_amt
) is not greater than the current balance of the safe (safeOwner[_safeId].balance
).If the requested amount is valid, it transfers the specified amount (
_amt
) from a token contract (ERC-20 token) to the provided address (_addr
) using thetransfer
function.After the transfer, it emits an event called
widthrawMoney
with the withdrawn amount (_amt
), the recipient address (_addr
), and the safe ID (_safeId
).It resets the status flags
sts1
andsts2
tofalse
for the safe identified by_safeId
so that the function can't be exploited.It updates the balance of the safe by subtracting the withdrawn amount (
_amt
) from the current balance (safeOwner[_safeId].balance
).It records the current timestamp (
block.timestamp
) in thetimestamps
array associated with the safe.
We have now completed the first part of this tutorial. In the upcoming second part, we will focus on developing another contract that will be deployed on a different blockchain network than the base chain where the first contract resides. This second contract will facilitate the process of obtaining the second owner's signature for the safe, which is a crucial step in the overall workflow.
If you have any doubts ,feel free to reach out to me on Telegram
Full code link for this smart contract can be found here : Part 1 Cross Chain Multisig contract
Subscribe to my newsletter
Read articles from Shubham Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Shubham Patel
Shubham Patel
Integration Engineer at Push Protocol.