paint-brush
WorldID on Gnosis? This Cross-Chain System Makes It Happenby@thebojda

WorldID on Gnosis? This Cross-Chain System Makes It Happen

by Laszlo FazekasDecember 4th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Gnosis uses the Arbitrary Message Bridge to send messages between two blockchains. The AMB is a system composed of smart contracts and external validators.
featured image - WorldID on Gnosis? This Cross-Chain System Makes It Happen
Laszlo Fazekas HackerNoon profile picture

There are numerous scenarios where it might be necessary for smart contracts on two different blockchains to communicate with each other. A typical example is when we want to move assets, such as ERC20 tokens, from one chain to another. However, this applies to any kind of state transfer as well. Personally, I started working with the AMB because I was curious about how WorldID could be made available on the Gnosis chain. When someone registers in the World network, their identifier is added to a Merkle tree. To support WorldID, it’s sufficient to synchronize the root of this tree between the chains, but this must be done in a highly reliable manner.


Anyone interested in learning more about how WorldID works can read my article on the topic.

Such cross-chain messaging can be implemented through bridges, and each blockchain has its own solution for this. Gnosis's solution is the Arbitrary Message Bridge, or AMB for short.


The AMB is a system composed of smart contracts and external validators. When we want to send a message from an external chain (e.g., Ethereum) to Gnosis, we can do so through the AMB contract deployed on Ethereum.


In the AMB contract's requireToPassMessage method, we provide the address of the target contract on Gnosis, along with the method we want to invoke and its parameters.


The AMB contract emits a UserRequestForAffirmation event when the method is called, which is monitored by the bridge validators. If 50% of the validators agree, they forward the data to the AMB contract on Gnosis, which then invokes the specified method on the target contract. This solution ensures that messages cannot be forged, as it would require at least half of the validators to act maliciously.


This process is illustrated in the diagram below, with the white arrows indicating the movement of the message from the Ethereum chain towards the Gnosis chain.


source: https://docs.gnosischain.com/bridges/About%20Token%20Bridges/amb-bridge


After the theory, let’s see how this works in practice. We’ll create a bridge to send messages from the Sepolia testnet (Ethereum) to the Chiado testnet (Gnosis). The project’s source code is available on GitHub.


First, let’s take a look at the MessageSender contract, which represents the Ethereum (Sepolia) side of the bridge and is used for sending messages.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

interface IAMB {
    function requireToPassMessage(
        address _contract,
        bytes calldata _data,
        uint256 _gas
    ) external returns (bytes32);
}

contract MessageSender {
    IAMB public amb;
    address public receiverContract; // Address of the contract on Gnosis Chain
    address public owner; // Address of the contract owner

    constructor(address _amb) {
        amb = IAMB(_amb);
        owner = msg.sender; // Set the contract deployer as the owner
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Caller is not the owner");
        _;
    }

    function setReceiverContract(address _receiverContract) public onlyOwner {
        receiverContract = _receiverContract;
    }

    function sendMessage(string memory _message) public {
        require(receiverContract != address(0), "Receiver contract not set");
        bytes4 methodSelector = bytes4(keccak256("receiveMessage(string)"));
        bytes memory data = abi.encodeWithSelector(methodSelector, _message);
        uint256 gasLimit = 200000; // Adjust based on the complexity of receiveMessage on Gnosis Chain
        amb.requireToPassMessage(receiverContract, data, gasLimit);
    }
}


The contract receives the address of the AMB contract in its constructor. For Sepolia, this address is 0xf2546D6648BD2af6a008A7e7C1542BB240329E11. The addresses of the various AMB contracts can be found in the Gnosis documentation.


The setReceiverContract method allows setting the address of the target contract, i.e., the address of the bridge's Gnosis side.


The sendMessage method is used to send the message. The method's parameter is the message, which is packaged with the signature of the target method using encodeWithSelector, and then the requireToPassMessage method of the AMB is called. From this point, the data forwarding is handled by the AMB contract and the validators.


Now, let’s look at the Gnosis side of the bridge, which is implemented by the MessageReceiver contract.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

interface IAMB {
    function messageSender() external view returns (address);
}

contract MessageReceiver {
    IAMB public amb;
    address public trustedSender; // Address of the MessageSender contract on Ethereum

    event MessageReceived(string message);

    constructor(address _amb, address _trustedSender) {
        amb = IAMB(_amb);
        trustedSender = _trustedSender;
    }

    function receiveMessage(string memory _message) public {
        require(msg.sender == address(amb), "Caller is not the AMB");
        require(amb.messageSender() == trustedSender, "Invalid message sender");

        emit MessageReceived(_message);

        // Implement additional logic to process the received message
    }
}


The contract's constructor has two parameters: one is the address of the AMB contract, and the other is the Sepolia address of the MessageSender contract.


The message is received by the receiveMessage method. This method can only be called by the AMB. After the call, the method verifies that the message sender is the MessageSender contract, i.e., the Sepolia side of the bridge. If all checks pass, a MessageReceived event is emitted.


The following TypeScript code deploys the two contracts mentioned above to the Sepolia and Chiado testnets. The script can be executed using the command npx hardhat run scripts/deploy.ts.


import { ethers } from "hardhat";

async function main() {
  // Deploy MessageSender on Sepolia
  const sepoliaProvider = new ethers.providers.JsonRpcProvider(process.env.SEPOLIA_RPC_URL);
  const sepoliaWallet = new ethers.Wallet(process.env.PRIVATE_KEY || "", sepoliaProvider);

  const MessageSender = await ethers.getContractFactory("MessageSender", sepoliaWallet);
  const sender = await MessageSender.deploy("0xf2546D6648BD2af6a008A7e7C1542BB240329E11");
  await sender.deployed();
  console.log(`MessageSender deployed to Sepolia at: ${sender.address}`);

  // Deploy MessageReceiver on Chiado
  const chiadoProvider = new ethers.providers.JsonRpcProvider(process.env.CHIADO_RPC_URL);
  const chiadoWallet = new ethers.Wallet(process.env.PRIVATE_KEY || "", chiadoProvider);

  const MessageReceiver = await ethers.getContractFactory("MessageReceiver", chiadoWallet);
  const receiver = await MessageReceiver.deploy("0x8448E15d0e706C0298dECA99F0b4744030e59d7d", sender.address);
  await receiver.deployed();
  console.log(`MessageReceiver deployed to Chiado at: ${receiver.address}`);

  // Update MessageSender with the receiver's address
  const tx = await sender.setReceiverContract(receiver.address);
  await tx.wait();
  console.log(`MessageSender's receiver contract set to: ${receiver.address}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});


The first block deploys the MessageSender to Sepolia. The next block deploys the MessageReceiver to Chiado, passing the Ethereum address of the sender contract as a parameter. Finally, the setReceiverContract method is used to set the receiver contract's address in the sender contract.


Once the deployment is complete, we can test it using the sendMessage.ts script.


import { ethers } from "ethers";
import * as dotenv from "dotenv";
import MessageSenderABI from "../artifacts/contracts/MessageSender.sol/MessageSender.json";
import MessageReceiverABI from "../artifacts/contracts/MessageReceiver.sol/MessageReceiver.json";

dotenv.config();

async function main() {
  // Replace with your deployed contract addresses
  const messageSenderAddress = process.env.SENDER_CONTRACT as string;
  const messageReceiverAddress = process.env.RECEIVER_CONTRACT as string;

  // Connect to the Sepolia network
  const sepoliaProvider = new ethers.providers.JsonRpcProvider(process.env.SEPOLIA_RPC_URL);
  const sepoliaWallet = new ethers.Wallet(process.env.PRIVATE_KEY || "", sepoliaProvider);

  // Connect to the deployed MessageSender contract
  const messageSender = new ethers.Contract(
    messageSenderAddress,
    MessageSenderABI.abi,
    sepoliaWallet
  );

  // Send a message
  const message = "Hello, Gnosis Chain!";
  const tx = await messageSender.sendMessage(message);
  await tx.wait();
  console.log(`Message sent: ${message}`);

  // Connect to the Chiado network
  const chiadoProvider = new ethers.providers.JsonRpcProvider(process.env.CHIADO_RPC_URL);

  // Connect to the deployed MessageReceiver contract
  const messageReceiver = new ethers.Contract(
    messageReceiverAddress,
    MessageReceiverABI.abi,
    chiadoProvider
  );

  // Listen for the MessageReceived event
  messageReceiver.once("MessageReceived", (receivedMessage: string) => {
    console.log(`Message received on Chiado: ${receivedMessage}`);
  });

  console.log("Waiting for the message to be received on Chiado...");

  // Keep the script running to listen for the event
  await new Promise((resolve) => setTimeout(resolve, 60000 * 20)); // Wait for 20 mins
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});


The script calls the sendMessage method of the MessageSender on Sepolia, then waits until the MessageReceived event of the receiver contract on Gnosis is triggered.


That's all. Thanks to Gnosis's AMB solution, we can relatively easily transfer messages between any external chain and Gnosis. This solution can be used for simple state transfers as well as more complex token transfer implementations. Gnosis's own OmniBridge token bridge solution is also built on the AMB.


Happy coding!