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