Source Code
- frax-oft-upgradeable (opens in a new tab): OFTs, lockboxes, and their LayerZero configuration
- hop-v2 (opens in a new tab): HopV2 contracts that route transfers through Fraxtal
- fraxtal-lz-hop (opens in a new tab): first-generation hops (paused) and the
MintRedeemHopcontracts - layerzero-v2-upgradeable (opens in a new tab): upgradeable fork of the LayerZero V2 OApp and OFT contracts
- frax-oft-legacy (opens in a new tab): legacy (non-upgradeable) OFTs
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-upgradeableimport { 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) asoft, and approve the Ethereum token. - Amounts are floored to the OFT's shared decimals (
decimalConversionRate, typically1e12) before sending. - Use the six-argument
sendOFT(oft, dstEid, recipient, amount, dstGas, data)to call a contract on the destination. The recipient must implementIHopComposer.hopCompose, anddstGasis raised to at least 400,000. - On Tempo, the Hop takes no
msg.valueand collects fees in a stablecoin instead, see Tempo.