The main difference in Sophon is that bridging tokens out requires the use of an Helper contract during this initial phase, as $SOPH is not distributed yet. The section below shows the difference from the LayerZero docs, the remaining steps (e.g. deploying an OFT token or bridging tokens in) are the same.

Calling send

LzOftHelper contract: 0x88172F3041Bd0787520dbc9Bd33D3d48e1fb46dc (on both mainnet and testnet)

You will need to contact product@sophon.xyz first to get your OFT token whitelisted on the LzOftHelper contract.

For native OFT tokens:

  1. Approve the OFT token amount to the LzOftHelper contract
  2. Call LzOftHelper.send(oftContract, IOFT.SendParam) (the quoteSend step is not needed)

For tokens using the OFT Adapter:

  1. approve the underlying token to the LzOftHelper contract
  2. Call LzOftHelper.send(OFTAdapter, IOFT.SendParam) (the quoteSend step is not needed)

Below the full send flow.

import { getNetworkNameForEid, types } from '@layerzerolabs/devtools-evm-hardhat';
import type { EndpointId } from '@layerzerolabs/lz-definitions';
import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities';
import { Options } from '@layerzerolabs/lz-v2-utilities';
import type { BigNumberish, BytesLike } from 'ethers';
import { task } from 'hardhat/config';

// LzOftHelper ABI (only the functions we need)
const HELPER_ABI = [
  "function send(address oftContract, tuple(uint32 dstEid, bytes to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external returns (tuple(bytes32 guid, uint256 nonce, bytes32 recipient, bytes message), tuple(uint256 amountLD, uint256 minAmountLD))",
  "function addressToBytes32(address _addr) external pure returns (bytes32)"
];

// Helper contract address
const HELPER_ADDRESS = "0x88172F3041Bd0787520dbc9Bd33D3d48e1fb46dc";

interface Args {
  amount: string;
  to: string;
  toEid: EndpointId;
  sophon?: boolean;
}

interface SendParam {
  dstEid: EndpointId; // Destination endpoint ID, represented as a number.
  to: BytesLike; // Recipient address, represented as bytes.
  amountLD: BigNumberish; // Amount to send in local decimals.
  minAmountLD: BigNumberish; // Minimum amount to send in local decimals.
  extraOptions: BytesLike; // Additional options supplied by the caller to be used in the LayerZero message.
  composeMsg: BytesLike; // The composed message for the send() operation.
  oftCmd: BytesLike; // The OFT command to be executed, unused in default OFT implementations.
}

// send tokens from a contract on one network to another
task('lz:oft:send', 'Sends tokens from either OFT or OFTAdapter')
  .addParam('to', 'contract address on network B', undefined, types.string)
  .addParam('toEid', 'destination endpoint ID', undefined, types.eid)
  .addParam('amount', 'amount to transfer in token decimals', undefined, types.string)
  .addOptionalParam('sophon', 'use LzOftHelper for Sophon network', false, types.boolean)
  .setAction(async (taskArgs: Args, { ethers, deployments }) => {
      const toAddress = taskArgs.to;
      const eidB = taskArgs.toEid;

      // Get the contract factories
      const oftDeployment = await deployments.get('MyOFT');

      const [signer] = await ethers.getSigners();

      // Create contract instances
      const oftContract = new ethers.Contract(oftDeployment.address, oftDeployment.abi, signer);
      const helperContract = new ethers.Contract(HELPER_ADDRESS, HELPER_ABI, signer);

      const decimals = await oftContract.decimals();
      const amount = ethers.utils.parseUnits(taskArgs.amount, decimals);
      const options = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes();

      // Now you can interact with the correct contract
      const oft = oftContract;

      const sendParam: SendParam = {
          dstEid: eidB,
          to: addressToBytes32(toAddress),
          amountLD: amount,
          minAmountLD: amount,
          extraOptions: options,
          composeMsg: ethers.utils.arrayify('0x'), // Assuming no composed message
          oftCmd: ethers.utils.arrayify('0x'), // Assuming no OFT command is needed
      };
      console.log(
          `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`
      );

      if (taskArgs.sophon) {
          const innerTokenAddress = await oft.token();
          const ERC20Factory = await ethers.getContractFactory('ERC20');
          const innerToken = ERC20Factory.attach(innerTokenAddress);

          // Approve tokens to helper contract
          await innerToken.approve(helperContract.address, amount);

          // Send via helper
          const tx = await helperContract.send(oftContract.address, sendParam);
          console.log(`Send tx initiated via helper. See: https://layerzeroscan.com/tx/${tx.hash}`);
      } else {
          // Original direct sending logic
          const feeQuote = await oft.quoteSend(sendParam, false);
          const nativeFee = feeQuote.nativeFee;
      
          console.log(
            `sending ${taskArgs.amount} token(s) to network ${getNetworkNameForEid(eidB)} (${eidB})`,
          );
      
          const ERC20Factory = await ethers.getContractFactory('ERC20');
          const innerTokenAddress = await oft.token();
      
          // // If the token address !== address(this), then this is an OFT Adapter
          // if (innerTokenAddress !== oft.address) {
          //     // If the contract is OFT Adapter, get decimals from the inner token
          //     const innerToken = ERC20Factory.attach(innerTokenAddress);
      
          //     // Approve the amount to be spent by the oft contract
          //     await innerToken.approve(oftDeployment.address, amount);
          // }
      
          const r = await oft.send(sendParam, {nativeFee: nativeFee, lzTokenFee: 0}, signer.address, {
            value: nativeFee,
          });
          console.log(`Send tx initiated directly. See: https://layerzeroscan.com/tx/${r.hash}`);
      }
  });