Build on the Synapse Interchain Network

The Synapse Interchain Network (SIN) is designed to be as easy as possible for developers to integrate. To this end, we've developed an abstract contract called MessageRecipient that provides much of the already minimal scaffolding required to get started sending cross-chain messages. If you don't want to use this, you can also implement the IMessageRecipient interface

You can also consider using BaseClient to avoid implementing any receiver checks yourself.

The contract also does some basic checks for us when receiving a message. Let's take a look at the receiveBaseMessage related functions which are used to receive a message from a destination:

/**
 * @notice Message recipient needs to implement this function in order to
 * receive cross-chain messages.
 * @dev Message recipient needs to ensure that merkle proof for the message
 * is at least as old as the optimistic period that the recipient is using.
 * Note: as this point it is checked that the "message optimistic period" has passed,
 * however the period value itself could be anything, and thus could differ from the one
 * that the recipient would like to enforce.
 * @param origin            Domain where message originated
 * @param nonce             Message nonce on the origin domain
 * @param sender            Sender address on origin chain
 * @param proofMaturity     Message's merkle proof age in seconds
 * @param version           Message version specified by sender
 * @param content           Raw bytes content of message
 */
function receiveBaseMessage(
    uint32 origin_,
    uint32 nonce,
    bytes32 sender,
    uint256 proofMaturity,
    uint32 version,
    bytes memory content
) external payable {
    if (msg.sender != destination) revert CallerNotDestination();
    if (nonce == 0) revert IncorrectNonce();
    if (sender == 0) revert IncorrectSender();
    if (proofMaturity == 0) revert ZeroProofMaturity();
    _receiveBaseMessageUnsafe(origin_, nonce, sender, proofMaturity, version, content);
}

/**
 * @dev Child contracts should implement the logic for receiving a Base Message in an "unsafe way".
 * Following checks HAVE been performed:
 *  - receiveBaseMessage() was called by Destination (i.e. this is a legit base message).
 *  - Nonce is not zero.
 *  - Message sender on origin chain is not a zero address.
 *  - Proof maturity is not zero.
 * Following checks HAVE NOT been performed (thus "unsafe"):
 *  - Message sender on origin chain could be anything non-zero at this point.
 *  - Proof maturity could be anything non-zero at this point.
 */
function _receiveBaseMessageUnsafe(
    uint32 origin_,
    uint32 nonce,
    bytes32 sender,
    uint256 proofMaturity,
    uint32 version,
    bytes memory content
) internal virtual;

This means in our test client we're going to have to implement some checks. The first thing you'll notice is both the origin and destination are pre-defined so you'll have to define those in the constructor like so using the appropriate origin and destination chain addresses for your chain.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {TypeCasts} from "../libs/TypeCasts.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {MessageRecipient} from "./MessageRecipient.sol";

contract TestClient is MessageRecipient {
    constructor(address origin_, address destination_) MessageRecipient(origin_, destination_) {}
}

Next up, you're going to want to define the sender checks and make sure the optimistic seconds period is set correctly. Let's define _receiveBaseMessageUnsafe

uint256 private constant REQUIRED_OPTIMISTIC_SECONDS = 20

error ProofMaturityTooLow();
error InvalidSender();

/// @inheritdoc MessageRecipient
function _receiveBaseMessageUnsafe(
    uint32 origin_,
    uint32 nonce,
    bytes32 sender,
    uint256 proofMaturity,
    uint32 version,
    bytes memory content
) internal override {
    // let's make sure everything is kosher before we accept the message
    
    // first we want to make sure the sender is who we think it is.
    if Typecast.addressToBytes32(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045) != sender {
        revert InvalidSender();
    }
    
    // and we want to make sure the optimistic seconds period has exceeded
    // what we want on our side.
    if (proofMaturity != REQUIRED_OPTIMISTIC_SECONDS) {
        revert ProofMaturityTooLow();
    }
    
    emit MessageReceived(origin_, nonce, sender, proofMaturity, version, content);
}

Now we're ready to receive a message, but we still have to send one. Let's define a sendMessage function:

function sendMessage(
    uint32 destination_,
    address recipientAddress,
    uint32 optimisticSeconds,
    uint64 gasLimit,
    uint32 version,
    bytes memory content
) external payable {
    // receipient is the address of this contract, but on the other chain
    bytes32 recipient = TypeCasts.addressToBytes32(recipientAddress);
    // if you want the user to get dropped gas on the destination chain, can define that here
    MessageRequest memory request = MessageRequest({gasDrop: 0, gasLimit: gasLimit, version: version});
    (uint32 nonce,) = _sendBaseMessage(destination_, recipient, optimisticSeconds, request, content);
    // you've sent he message
    emit MessageSent(destination_, nonce, TypeCasts.addressToBytes32(address(this)), recipient, content);
}

Feel free to reach out in Discord for more help integrating these contracts. As the ecosystem matures, more dev docs will be released as well as more integrations.

In the meantime, you can see examples here and here.

Last updated