How to Swap

Use Stargate to transfer an asset across blockchains.

Overview

Stargate V2 allows for same-asset bridging only, which means that USDC on Ethereum can only be swapped with USDC on some other chain.

When performing a swap you have two main options for balancing speed and gas costs:

  1. Taking a taxi: Immediately performs a swap and sends an omnichain message to the destination chain.

  2. Riding the bus: Allows the user to take advantage of cheaper gas costs thanks to transaction batching. When you use this approach your swap will immediately be settled on the local chain with instant guaranteed finality. However, you may need to wait before you receive the target asset on the destination chain. The message will be sent to destination chain when a "bus" reaches a set number of passengers (between 2-10). An impatient user can also choose to driveBus, by buying up the remaining bus tickets.

Instant guaranteed finality ensures that your swap will be executed, even when taking the bus.

OFT standard

As a reminder Stargate V2 interfaces are built upon the IOFT interface for OFTs on LayerZero V2. IOFT interface is available here. Documentation for building on IOFT is here.

This means that when you execute a swap on Stargate you are actually calling OFT.send(). Lets take a look at IStargate interface that extends OFT standard:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { IOFT, SendParam, MessagingFee, MessagingReceipt, OFTReceipt } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol";

enum StargateType {
    Pool,
    OFT
}

struct Ticket {
    uint56 ticketId;
    bytes passenger;
}

/// @title Interface for Stargate.
/// @notice Defines an API for sending tokens to destination chains.
interface IStargate is IOFT {
    /// @dev This function is same as `send` in OFT interface but returns the ticket data if in the bus ride mode,
    /// which allows the caller to ride and drive the bus in the same transaction.
    function sendToken(
        SendParam calldata _sendParam,
        MessagingFee calldata _fee,
        address _refundAddress
    ) external payable returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt, Ticket memory ticket);

    /// @notice Returns the Stargate implementation type.
    function stargateType() external pure returns (StargateType);
}

As you can see above the Stargate interface isn't much different from an OFT. The most important piece of the interface is:

SendParam calldata _sendParam

Let's explain it:

/**
 * @dev Struct representing token parameters for the OFT send() operation.
 */
struct SendParam {
    uint32 dstEid; // Destination endpoint ID.
    bytes32 to; // Recipient address.
    uint256 amountLD; // Amount to send in local decimals.
    uint256 minAmountLD; // Minimum amount to send in local decimals.
    bytes extraOptions; // Additional options supplied by the caller to be used in the LayerZero message.
    bytes composeMsg; // The composed message for the send() operation.
    bytes oftCmd; // The OFT command to be executed, unused in default OFT implementations.
}

The biggest Stargate specific difference is the use of last three properties of the struct above.

SendParam.extraOptions

extraOptions

If you use taxi mode then these options are LayerZero's execution options. You can use OptionsBuilder to prepare them. The exception to the above is that you don't need to put addExecutorLzReceiveOption() in them, because it is handled automatically by Stargate.

If you use bus mode these are options from RideBusParams:

struct RideBusParams {
    address sender;
    uint32 dstEid;
    bytes32 receiver;
    uint64 amountSD;
    bool nativeDrop;
}

SendParam.composeMsg

Check the Composability page to learn more about it. If you don't plan to use any destination logic, feel free to just use: new bytes(0).

SendParam.oftCmd

The OFT command to be executed, unused in default OFT implementation, but in Stargate it is used to indicate the transportation mode.

pragma solidity ^0.8.22;

library OftCmdHelper {
    function taxi() internal pure returns (bytes memory) {
        return "";
    }

    function bus() internal pure returns (bytes memory) {
        return new bytes(1);
    }

    function drive(bytes memory _passengers) internal pure returns (bytes memory) {
        return _passengers;
    }
}

Empty bytes are "taxi", bytes(1) is riding a bus. If you pass a bytes array of _passengers it indicates you want to drive a bus.

Take a taxi

Let's start with the first high-level example.

Users can opt to pay for their transactions to be bridged immediately by taking a taxi. Take a look at prepareTakeTaxi() in the code example below:

pragma solidity ^0.8.19;

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

contract StargateIntegration {
    function prepareTakeTaxi(
        address _stargate,
        uint32 _dstEid,
        uint256 _amount,
        address _receiver
    ) external view returns (uint256 valueToSend, SendParam memory sendParam, MessagingFee memory messagingFee) {
        sendParam = SendParam({
            dstEid: _dstEid,
            to: addressToBytes32(_receiver),
            amountLD: _amount,
            minAmountLD: _amount,
            extraOptions: new bytes(0),
            composeMsg: new bytes(0),
            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)));
    }
}

...and the following code initiates an omnichain transaction using the example Alice account:

StargateIntegration integration = new StargateIntegration();

// as Alice
ERC20(sourceChainPoolToken).approve(stargate, amount);

(uint256 valueToSend, SendParam memory sendParam, MessagingFee memory messagingFee) =
    integration.prepareTakeTaxi(stargate, destinationEndpointId, amount, alice);

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

The above code executes a swap and requests an immediate taxi ride of the assets to the destination chain.

Ride the bus

Here is an example of how to perform an omnichain swap using Solidity and bus mode:

pragma solidity ^0.8.19;

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

contract StargateIntegration {
    function prepareRideBus(
        address _stargate,
        uint32 _dstEid,
        uint256 _amount,
        address _receiver
    ) external view returns (uint256 valueToSend, SendParam memory sendParam, MessagingFee memory messagingFee) {
        sendParam = SendParam({
            dstEid: _dstEid,
            to: addressToBytes32(_receiver),
            amountLD: _amount,
            minAmountLD: _amount,
            extraOptions: new bytes(0),
            composeMsg: new bytes(0),
            oftCmd: new bytes(1)
        });

        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)));
    }
}

The function prepareRideBus contains logic to interact with Stargate and prepare arguments for token swap. You can send the swap transaction using code below:

StargateIntegration integration = new StargateIntegration();

// as Alice
ERC20(sourceChainPoolToken).approve(stargate, amount);

(uint256 valueToSend, SendParam memory sendParam, MessagingFee memory messagingFee) =
    integration.prepareRideBus(stargate, destinationEndpointId, amount, alice);

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

The bus ride isn't instant. The swap is locally settled instantly, but you need to wait to receive tokens on the destination chain because bus transactions are batched together.

Bus Ticket

When you board a bus, you will receive a Ticket for your journey to the destination chain. This ticket can be acquired by capturing parameters from the sendToken() function.

(, , Ticket memory ticket) = stargate.sendToken{ value: valueToSend }(sendParam, messagingFee, ALICE);

Ticket is a struct:

struct Ticket {
    uint56 ticketId;
    bytes passenger;
}

Checking if the Bus has already left

If you want to check whether your bus has already left, compare your Ticket.ticketId with busQueues[dstEid].nextTicketId. If the nextTicketId is greater than your ticketId it means that your tokens were sent to the destination chain.

Last updated