Skip to Content

How to Transfer


Overview

Stargate V2 allows for same-asset bridging only, meaning 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:

  • Taking a taxi: Immediately performs a swap and sends an omnichain message to the destination chain.
  • 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 the 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.

  • The 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(). Let’s take a look at the IStargate interface that extends the 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 the last three properties of the struct above.

SendParam.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.

The practical example of using extraOptions can be found in Composability section where addExecutorLzComposeOption() is used to enable composing functionality

Another interesting option is addExecutorNativeDropOption() which can be used to drop native tokens to the address you specify.

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. While unused in default OFT implementations, Stargate uses it to indicate the transportation mode:

pragma solidity ^0.8.22; library OftCmdHelper { function taxi() internal pure returns (bytes memory) { return ""; // Empty bytes for "taxi" } function bus() internal pure returns (bytes memory) { return new bytes(1); // bytes(1) for riding a bus } function drive(bytes memory _passengers) internal pure returns (bytes memory) { return _passengers; // bytes array of _passengers to drive a bus } }

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

Users can opt to pay for their transactions to be bridged immediately by “taking a taxi.” The prepareTakeTaxi() function below illustrates how to prepare for this:

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, // Will be updated with quote extraOptions: new bytes(0), // Default, can be customized composeMsg: new bytes(0), // Default, can be customized oftCmd: "" // Empty for taxi mode }); IStargate stargate = IStargate(_stargate); // Get accurate minimum amount from quote (, , OFTReceipt memory receipt) = stargate.quoteOFT(sendParam); sendParam.minAmountLD = receipt.amountReceivedLD; // Get messaging fee messagingFee = stargate.quoteSend(sendParam, false); // false for not paying with ZRO valueToSend = messagingFee.nativeFee; // If sending native gas token, add amount to valueToSend if (stargate.token() == address(0x0)) { valueToSend += sendParam.amountLD; } } function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } }

The following code initiates an omnichain transaction:

// Assuming Alice's address and necessary variables are defined // address alice = /* Alice's address */; // address stargate = /* Stargate contract address */; // address sourceChainPoolToken = /* Token address on source chain */; // uint256 amount = /* Amount to send */; // uint32 destinationEndpointId = /* Destination chain's LayerZero Endpoint ID */; 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); // Use alice as refundAddress

This executes a transfer and requests an immediate “taxi ride” of the assets.



Ride the Bus

To perform an omnichain transfer using “bus mode” (batched transactions for potentially lower gas costs):

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, // Will be updated with quote extraOptions: new bytes(0), // Default, can be customized composeMsg: new bytes(0), // Default, can be customized oftCmd: new bytes(1) // bytes(1) for bus mode }); IStargate stargate = IStargate(_stargate); // Get accurate minimum amount from quote (, , OFTReceipt memory receipt) = stargate.quoteOFT(sendParam); sendParam.minAmountLD = receipt.amountReceivedLD; // Get messaging fee messagingFee = stargate.quoteSend(sendParam, false); // false for not paying with ZRO valueToSend = messagingFee.nativeFee; // If sending native gas token, add amount to valueToSend if (stargate.token() == address(0x0)) { valueToSend += sendParam.amountLD; } } function addressToBytes32(address _addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(_addr))); } }

To send the transfer transaction:

// Assuming Alice's address and necessary variables are defined // address alice = /* Alice's address */; // address stargate = /* Stargate contract address */; // address sourceChainPoolToken = /* Token address on source chain */; // uint256 amount = /* Amount to send */; // uint32 destinationEndpointId = /* Destination chain's LayerZero Endpoint ID */; 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); // Use alice as refundAddress

The bus ride isn’t instant. The transfer is locally settled with instant guaranteed finality, but you need to wait to receive tokens on the destination chain because bus transactions are batched. Bus Ticket When you board a bus, you receive a Ticket for your journey:

// Assuming 'stargate', 'valueToSend', 'sendParam', 'messagingFee', and 'alice' are defined (, , Ticket memory ticket) = IStargate(stargate).sendToken{ value: valueToSend }(sendParam, messagingFee, alice);

The Ticket struct:

struct Ticket { uint56 ticketId; bytes passenger; // Contains data for driving the bus if initiated by this sender }

Checking if the Bus has Departed

To check if your bus has left, compare your Ticket.ticketId with busQueues[dstEid].nextTicketId (a public mapping on the Stargate contract). If nextTicketId is greater than your ticketId, your tokens were sent.




Further Reading


Last updated on