Protocol
Crosschain Strategy
Code

Source Code

You can bridge programatically through Solidity and Typescript.

A Frax OFT sends directly only between Fraxtal and another chain. To move tokens between two chains other than Fraxtal, use HopV2, which relays through Fraxtal from a single transaction.

NOTE: When sending from Ethereum or Fraxtal, as you are sending the Frax asset to the lockbox, it is required to first approve the lockbox contract to transfer the asset on your behalf.

extraOptions can be left empty. Every Frax OFT applies enforced options that forward enough gas to the destination chain.

Solidity

npm install @fraxfinance/layerzero-v2-upgradeable
import { OptionsBuilder } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oapp/libs/OptionsBuilder.sol";
import { SendParam, MessagingFee, IOFT } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol";
 
uint256 amount = 1e18;
// frxUSD OFT - Bridging FROM semi-deterministic chain
address oft = 0x80Eede496655FB9047dd39d9f418d5483ED600df;
// Fraxtal - choose destination EID from https://github.com/FraxFinance/frax-oft-upgradeable/blob/master/scripts/L0Config.json
uint32 dstEid = 30255;
 
bytes memory options = OptionsBuilder.newOptions();
SendParam memory sendParam = SendParam({
        dstEid: dstEid,
        to: bytes32(uint256(uint160(msg.sender))),
        amountLD: amount,
        minAmountLD: amount,
        extraOptions: options,
        composeMsg: '',
        oftCmd: ''
});
MessagingFee memory fee = IOFT(oft).quoteSend(sendParam, false);
IOFT(oft).send{value: fee.nativeFee}(
    sendParam,
    fee,
    payable(msg.sender)
);

Typescript

import * as dotenv from "dotenv";
import { ethers } from "ethers";
import { Options } from "@layerzerolabs/lz-v2-utilities";
 
const abi = [
  // load the full ABI from https://arbiscan.io/address/0x80Eede496655FB9047dd39d9f418d5483ED600df#code
  {
    inputs: [
      {
        components: [
          { internalType: "uint32", name: "dstEid", type: "uint32" },
          { internalType: "bytes32", name: "to", type: "bytes32" },
          { internalType: "uint256", name: "amountLD", type: "uint256" },
          { internalType: "uint256", name: "minAmountLD", type: "uint256" },
          { internalType: "bytes", name: "extraOptions", type: "bytes" },
          { internalType: "bytes", name: "composeMsg", type: "bytes" },
          { internalType: "bytes", name: "oftCmd", type: "bytes" },
        ],
        internalType: "struct SendParam",
        name: "_sendParam",
        type: "tuple",
      },
      { internalType: "bool", name: "_payInLzToken", type: "bool" },
    ],
    name: "quoteSend",
    outputs: [
      {
        components: [
          { internalType: "uint256", name: "nativeFee", type: "uint256" },
          { internalType: "uint256", name: "lzTokenFee", type: "uint256" },
        ],
        internalType: "struct MessagingFee",
        name: "msgFee",
        type: "tuple",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        components: [
          { internalType: "uint32", name: "dstEid", type: "uint32" },
          { internalType: "bytes32", name: "to", type: "bytes32" },
          { internalType: "uint256", name: "amountLD", type: "uint256" },
          { internalType: "uint256", name: "minAmountLD", type: "uint256" },
          { internalType: "bytes", name: "extraOptions", type: "bytes" },
          { internalType: "bytes", name: "composeMsg", type: "bytes" },
          { internalType: "bytes", name: "oftCmd", type: "bytes" },
        ],
        internalType: "struct SendParam",
        name: "_sendParam",
        type: "tuple",
      },
      {
        components: [
          { internalType: "uint256", name: "nativeFee", type: "uint256" },
          { internalType: "uint256", name: "lzTokenFee", type: "uint256" },
        ],
        internalType: "struct MessagingFee",
        name: "_fee",
        type: "tuple",
      },
      { internalType: "address", name: "_refundAddress", type: "address" },
    ],
    name: "send",
    outputs: [
      {
        components: [
          { internalType: "bytes32", name: "guid", type: "bytes32" },
          { internalType: "uint64", name: "nonce", type: "uint64" },
          {
            components: [
              { internalType: "uint256", name: "nativeFee", type: "uint256" },
              { internalType: "uint256", name: "lzTokenFee", type: "uint256" },
            ],
            internalType: "struct MessagingFee",
            name: "fee",
            type: "tuple",
          },
        ],
        internalType: "struct MessagingReceipt",
        name: "msgReceipt",
        type: "tuple",
      },
      {
        components: [
          { internalType: "uint256", name: "amountSentLD", type: "uint256" },
          {
            internalType: "uint256",
            name: "amountReceivedLD",
            type: "uint256",
          },
        ],
        internalType: "struct OFTReceipt",
        name: "oftReceipt",
        type: "tuple",
      },
    ],
    stateMutability: "payable",
    type: "function",
  },
];
 
dotenv.config();
const PK: string = process.env.PK!;
 
const oft = "0x80Eede496655FB9047dd39d9f418d5483ED600df"; // frxUSD on semi-deterministic chain
 
async function main() {
  const url = /* ENTER RPC URL HERE */;
  const provider = new ethers.providers.JsonRpcProvider(url);
  const signer = new ethers.Wallet(PK, provider);
  const contract = new ethers.Contract(oft, abi, signer);
 
  // basic send to Fraxtal
  const dstEid = 30255;
  const to = ethers.utils.zeroPad(signer.address, 32);
  const options = Options.newOptions().toHex().toString();
  const composeMsg = "0x";
 
  const amount = ethers.utils.parseEther("1");
  const minAmountLD = 0;
 
  const sendParam = [
    dstEid,
    to,
    amount.toString(),
    minAmountLD.toString(),
    options,
    composeMsg,
    "0x",
  ];
 
  // get native fee
  const [nativeFee] = await contract.quoteSend(sendParam, false);
 
  // execute the send
  await contract.send(sendParam, [nativeFee, 0], signer.address, {
    value: nativeFee,
  });
}
 
main();

Bridging through HopV2

HopV2 (opens in a new tab) sends an OFT between any two EVM chains in the mesh in a single transaction. Call sendOFT on the source chain's Hop: 0x0000006D38568b00B457580b734e0076C62de659 on every chain except Fraxtal, where the Hop is 0x00000000e18aFc20Afe54d4B2C8688bB60c06B36. See HopV2 Contracts for the full list.

interface IHopV2 {
    function quote(
        address oft,
        uint32 dstEid,
        bytes32 recipient,
        uint256 amountLD,
        uint128 dstGas,
        bytes memory data
    ) external view returns (uint256);
 
    function sendOFT(address oft, uint32 dstEid, bytes32 recipient, uint256 amountLD) external payable;
}
 
// Arbitrum -> Base, relayed through Fraxtal
address hop = 0x0000006D38568b00B457580b734e0076C62de659;
address oft = 0x80Eede496655FB9047dd39d9f418d5483ED600df; // frxUSD OFT on Arbitrum
uint32 dstEid = 30184; // Base
bytes32 recipient = bytes32(uint256(uint160(msg.sender)));
uint256 amount = 1e18;
 
// 1. Quote both legs (Arbitrum -> Fraxtal and Fraxtal -> Base)
uint256 fee = IHopV2(hop).quote(oft, dstEid, recipient, amount, 0, "");
 
// 2. Approve the underlying token, not the OFT
IERC20(IOFT(oft).token()).approve(hop, amount);
 
// 3. Send; any msg.value above the fee is refunded
IHopV2(hop).sendOFT{value: fee}(oft, dstEid, recipient, amount);
  • On Ethereum, pass the lockbox address (for example the frxUSD lockbox 0x566a6442A5A6e9895B9dCA97cC7879D632c6e4B0) as oft, and approve the Ethereum token.
  • Amounts are floored to the OFT's shared decimals (decimalConversionRate, typically 1e12) before sending.
  • Use the six-argument sendOFT(oft, dstEid, recipient, amount, dstGas, data) to call a contract on the destination. The recipient must implement IHopComposer.hopCompose, and dstGas is raised to at least 400,000.
  • On Tempo, the Hop takes no msg.value and collects fees in a stablecoin instead, see Tempo.