Composability

Stargate is a fully composable liquidity transport protocol meaning it supports additional compose logic on destination chain. You can use this feature to perform calls to external smart contracts.

Note: Only Stargate's taxi() method is composable, you cannot perform destination logic with rideBus().

Composable methods

The following methods are composable:

  1. IStargate.sendToken()

  2. IStargate.send()

  3. IStargatePool.redeemSend()

Architecture

Send

To take advantage of compose feature you need to modify SendParam struct passed to IStargate.sendToken().

First, you need to change:

bytes calldata composeMsg

Make sure it is non-zero bytes. You would usually use this field with ABI encode and decode to pass your application-specific input that contracts along the way understand.

You also need to pass additional gas for the compose call. You need to set this value to the amount of gas your lzCompose function in the compose receiver consumes.

For "taxi" you can use typical LayerZero's OptionsBuilder. Make sure to pass it as SendParam.extraOptions:

bytes memory extraOptions = _composeMsg.length > 0
    ? OptionsBuilder.newOptions().addExecutorLzComposeOption(0, 200_000, 0) // compose gas limit
    : bytes("");

Receive

Stargate will attempt to call LayerZero's Endpoint.sendCompose() on the destination chain when it distributes tokens to receiver.

Here's how the Stargate internal call looks like:

endpoint.sendCompose(_payload.receiver, _guid, _payload.composeIdx, composeMsg);

where composeMsg is:

composeMsg = OFTComposeMsgCodec.encode(_origin.nonce, _origin.srcEid, amountLD, _payload.composeMsg);

and receiver is an address that was supposed to receive tokens on the destination chain.

Stargate is using standard OFTComposeMsgCodec for encoding a composed message. This means that when you receive this message in the composer it will be encoded using the aforementioned codec.

To access your custom application specific message (the one you passed as SendParams.composeMsg) you have to call:

bytes memory _composeMessage = OFTComposeMsgCodec.composeMsg(_message);

Implementing "composer receiver"

To receive a composed message from Stargate and perform additional logic the receiver address has to be a smart contract implementing ILayerZeroComposer. LayerZero's Endpoint defaults to calling the lzCompose() function on the receiver contract address.

Here's how an example receiver can look like:

pragma solidity ^0.8.19;

import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol";

contract ComposerReceiver is ILayerZeroComposer {
    event ComposeAcknowledged(address indexed _from, bytes32 indexed _guid, bytes _message, address _executor, bytes _extraData);

    uint256 public acknowledgedCount;

    function lzCompose(
        address _from,
        bytes32 _guid,
        bytes calldata _message,
        address _executor,
        bytes calldata _extraData
    ) external payable {
        acknowledgedCount++;

        emit ComposeAcknowledged(_from, _guid, _message, _executor, _extraData);
    }

    fallback() external payable {}
    receive() external payable {}
}

This very simple example above will emit ComposeAcknowledged each time a composed call is received and increment the acknowledgedCount by 1. A more advanced contract example is detailed below.

External contract interaction example

Below you can find a Solidity example of doing a swap through external smart contract with a composed message, using the taxi method.

This example illustrates the process of swapping Token A on the source chain for Token B on the destination chain. Following this, it demonstrates how to swap Token B for Token C on the destination chain by leveraging an external smart contract through a composed call.

Send

Preparing arguments:

pragma solidity ^0.8.19;

import { IStargate, Ticket } from "@stargatefinance/stg-evm-v2/src/interfaces/IStargate.sol";
import { MessagingFee, OFTReceipt, SendParam } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol";
import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol";

contract StargateIntegrationWithCompose {
    using OptionsBuilder for bytes;
    
    function prepareTakeTaxiAndAMMSwap(
        address _stargate,
        uint32 _dstEid,
        uint256 _amount,
        address _composer,
        bytes memory _composeMsg
    ) external view returns (uint256 valueToSend, SendParam memory sendParam, MessagingFee memory messagingFee) {
        bytes memory extraOptions = _composeMsg.length > 0
            ? OptionsBuilder.newOptions().addExecutorLzComposeOption(0, 200_000, 0) // compose gas limit
            : bytes("");

        sendParam = SendParam({
            dstEid: _dstEid,
            to: addressToBytes32(_composer),
            amountLD: _amount,
            minAmountLD: _amount,
            extraOptions: extraOptions,
            composeMsg: _composeMsg,
            oftCmd: ""
        });

        IStargate stargate = IStargate(_stargate);

        (, , OFTReceipt memory receipt) = stargate.quoteOFT(sendParam);
        sendParam.minAmountLD = receipt.amountReceivedLD;

        messagingFee = stargate.quoteSend(sendParam, false);
        valueToSend = messagingFee.nativeFee;

        if (stargate.token() == address(0x0)) {
            valueToSend += sendParam.amountLD;
        }
    }

    function addressToBytes32(address _addr) internal pure returns (bytes32) {
        return bytes32(uint256(uint160(_addr)));
    }
}

Sending transaction:

bytes memory _composeMsg = abi.encode(_tokenReceiver, _oftOnDestination, _tokenOut, _amountOutMinDest, _deadline);

(uint256 valueToSend, SendParam memory sendParam, MessagingFee memory messagingFee) =
        integration.prepareTakeTaxiAndAMMSwap(stargate, destinationEndpointId, amount, address(composer), _composeMsg);
        
IStargate stargate = IStargate(stargate);

IStargate(stargate).sendToken{ value: valueToSend }(sendParam, messagingFee, refundAddress);

Receive

On the receive side we will use dummy IMockAMM interface to demonstrate how the external call to the swap function can look like:

pragma solidity ^0.8.19;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/libs/OFTComposeMsgCodec.sol";

import { IMockAMM } from './interfaces/IMockAMM.sol';

contract ComposerReceiverAMM is ILayerZeroComposer {
    IMockAMM public immutable amm;
    address public immutable endpoint;
    address public immutable stargate;

    event ReceivedOnDestination(address token);

    constructor(address _amm, address _endpoint, address _stargate) {
        amm = IMockAMM(_amm);
        endpoint = _endpoint;
        stargate = _stargate;
    }

    function lzCompose(
        address _from,
        bytes32 _guid,
        bytes calldata _message,
        address _executor,
        bytes calldata _extraData
    ) external payable {
        require(_from == stargate, "!stargate");
        require(msg.sender == endpoint, "!endpoint");

        uint256 amountLD = OFTComposeMsgCodec.amountLD(_message);
        bytes memory _composeMessage = OFTComposeMsgCodec.composeMsg(_message);

        (address _tokenReceiver, address _oftOnDestination, address _tokenOut, uint _amountOutMinDest, uint _deadline) =
            abi.decode(_composeMessage, (address, address, address, uint, uint));

        address[] memory path = new address[](2);
        path[0] = _oftOnDestination;
        path[1] = _tokenOut;

        IERC20(_oftOnDestination).approve(address(amm), amountLD);

        try amm.swapExactTokensForTokens(
            amountLD,
            _amountOutMinDest,
            path,  
            _tokenReceiver, 
            _deadline 
        ) {
            emit ReceivedOnDestination(_tokenOut);
        } catch {
            IERC20(_oftOnDestination).transfer(_tokenReceiver, amountLD);
            emit ReceivedOnDestination(_oftOnDestination);
        }
    }

    fallback() external payable {}
    receive() external payable {}
}

As shown above, lzCompose() will attempt to swap token B received from Stargate into token C and transfer it to the receiver address. If the swap fails in the try/catch clause it will send the original token B to the receiver address instead.

Last updated