> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
> Use this file to discover all available pages before exploring further.

# HyperEVM Testnet OFT Quickstart

> Deploy Omnichain Fungible Tokens on HyperEVM using LayerZero V2. Step-by-step OFT deployment guide with code examples. Build omnichain tokens with LayerZero V2.

Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **HyperEVM Testnet** and any other supported chain.

## Project scaffold

LayerZero's CLI lets you spin up an OFT workspace in seconds:

```bash wrap theme={null}
npx create-lz-oapp@latest       # choose → "OFT example"
```

The wizard creates a repo with Hardhat + Foundry, sample contracts, tests and LayerZero helper scripts.

## Add private keys

Rename `.env.example` file to `.env` and update it with needed configurations:

```js wrap theme={null}
PRIVATE_KEY = your_private_key; // Required
```

At a minimum, you need to have the `PRIVATE_KEY`. RPC URLs are optional, but strongly recommended. If you don't provide them, public RPCs will be used, but public RPCs can be unreliable or slow, leading to long waiting times for transactions to be confirmed or, at worst, cause your transactions to fail.

## Hardhat network config

Update your `hardhat.config.ts` file to include the networks you want to deploy your contracts to:

```ts wrap theme={null}
networks: {
  // the network you are deploying to or are already on
  // HyperEVM Testnet (EID=40362)
  'hyperliquid-testnet': {
    eid: EndpointId.HYPERLIQUID_V2_TESTNET,
    url: process.env.RPC_URL_HYPERLIQUID_TESTNET || 'https://rpc.hyperliquid-testnet.xyz/evm',
    accounts,
  },
  // another network you want to connect to
  'sepolia-testnet': {
    eid: EndpointId.ETHEREUM_V2_SEPOLIA,
    url: process.env.RPC_URL_SEPOLIA || 'https://rpc.sepolia.org',
    accounts,
  },
}
```

## LayerZero wiring config

Modify your `layerzero.config.ts` file to include the chains and channel security settings you want for each connection:

```ts wrap theme={null}
import {EndpointId} from '@layerzerolabs/lz-definitions';
import type {OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat';
import {OAppEnforcedOption} from '@layerzerolabs/toolbox-hardhat';
import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities';
import {TwoWayConfig, generateConnectionsConfig} from '@layerzerolabs/metadata-tools';

const hyperliquidTestnetContract: OmniPointHardhat = {
  eid: EndpointId.HYPERLIQUID_V2_TESTNET,
  contractName: 'MyOFT',
};

const sepoliaContract: OmniPointHardhat = {
  eid: EndpointId.ETHEREUM_V2_SEPOLIA,
  contractName: 'MyOFT',
};

// To connect all the above chains to each other, we need the following pathways:
// Sepolia <-> hyperliquid-testnet
// hyperliquid-testnet <-> Sepolia

// For this example's simplicity, we will use the same enforced options values for sending to all chains
// To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings
const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [
  {
    msgType: 1,
    optionType: ExecutorOptionType.LZ_RECEIVE,
    gas: 80000,
    value: 0,
  },
];

const pathways: TwoWayConfig[] = [
  [
    // 1) Chain B's contract (e.g. Sepolia)
    sepoliaContract,

    // 2) Chain A's contract (e.g. hyperliquid-testnet)
    hyperliquidTestnetContract,

    // 3) Channel security settings:
    //    • first array = "required" DVN names
    //    • second array = "optional" DVN names array + threshold
    //    • third value = threshold (i.e., number of optionalDVNs that must sign)
    //    [ requiredDVN[], [ optionalDVN[], threshold ] ]
    [['LayerZero Labs' /* ← add more DVN names here */], []],

    // 4) Block confirmations:
    //    [confirmations for Sepolia → hyperliquid-testnet, confirmations for hyperliquid-testnet → Sepolia]
    [20, 1],

    // 5) Enforced execution options:
    //    [options for Sepolia → hyperliquid-testnet, options for hyperliquid-testnet → Sepolia]
    [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS],
  ],
];

export default async function () {
  // Generate the connections config based on the pathways
  const connections = await generateConnectionsConfig(pathways);
  return {
    contracts: [{contract: sepoliaContract}, {contract: hyperliquidTestnetContract}],
    connections,
  };
}
```

<Warning>
  It is strongly recommended to review [**LayerZero's Channel Security Model**](../../concepts/protocol/message-security#layerzero's-channel-security-model) and understand the impact of each of these configuration settings.

  See [**Next Steps**](#nextsteps) to review the available providers and security settings.
</Warning>

## The token contract

```solidity wrap title="contracts/MyOFT.sol" theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { OFT }      from "@layerzerolabs/oft-evm/contracts/OFT.sol";

contract MyOFT is OFT, Ownable {
  constructor(string memory name, string memory symbol, address endpoint, address owner)
    OFT(name, symbol, endpoint, owner) Ownable(owner) {}
}
```

<Tip>
  The OFT contract uses the ERC20 token standard. You may want to add a `mint(...)` function in the `constructor(...)` or contract body if this is your first time deploying an OFT. If you have an existing ERC20 token, you will want to use an OFT Adapter contract.

  You can read the general [**OFT Quickstart**](../../developers/evm/oft/quickstart) for a better understanding of how OFTs work and what contracts to use.
</Tip>

## Deploy

```bash wrap theme={null}
npx hardhat lz:deploy                     # choose hyperliquid-testnet
```

You will be presented with a list of networks to deploy to.

Fund your deployer with native gas tokens beforehand.

## Connect the chains

```bash wrap theme={null}
npx hardhat lz:oapp:wire   --oapp-config layerzero.config.ts
```

Verify peers:

```bash wrap theme={null}
npx hardhat lz:oapp:peers:get --oapp-config layerzero.config.ts
```

## Transfer

### Calling `send`

Since the `send` logic has already been defined, we'll instead view how the function should be called.

<CodeGroup>
  ```typescript wrap Hardhat Task theme={null}
  import {task} from 'hardhat/config';
  import {getNetworkNameForEid, types} from '@layerzerolabs/devtools-evm-hardhat';
  import {EndpointId} from '@layerzerolabs/lz-definitions';
  import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities';
  import {Options} from '@layerzerolabs/lz-v2-utilities';
  import {BigNumberish, BytesLike} from 'ethers';

  interface Args {
    amount: string;
    to: string;
    toEid: EndpointId;
  }

  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 receive on destination in local decimals.
    extraOptions: BytesLike; // Additional options for the message.
    composeMsg: BytesLike; // Message to compose with the transfer.
    oftCmd: BytesLike; // OFT command to execute.
  }

  task('send', 'Send tokens to another chain')
    .addParam('amount', 'Amount to send', undefined, types.string)
    .addParam('to', 'Recipient address', undefined, types.string)
    .addParam('toEid', 'Destination endpoint ID', undefined, types.int)
    .setAction(async (args: Args, hre) => {
      const {amount, to, toEid} = args;
      const networkName = await getNetworkNameForEid(hre, toEid);
      console.log(`Sending ${amount} tokens to ${to} on ${networkName} (EID: ${toEid})`);

      const {MyOFT} = await hre.getNamedAccounts();
      const oft = await hre.ethers.getContract('MyOFT', MyOFT);

      const sendParam: SendParam = {
        dstEid: toEid,
        to: addressToBytes32(to),
        amountLD: hre.ethers.parseEther(amount),
        minAmountLD: hre.ethers.parseEther(amount),
        extraOptions: '0x',
        composeMsg: '0x',
        oftCmd: '0x',
      };

      const tx = await oft.send(sendParam, []);
      console.log(`Transaction hash: ${tx.hash}`);
      await tx.wait();
      console.log('Transaction confirmed!');
    });
  ```

  ```solidity wrap Contract Call theme={null}
  // SPDX-License-Identifier: UNLICENSED
  pragma solidity ^0.8.22;

  import {OFT} from "@layerzerolabs/oft-evm/contracts/OFT.sol";
  import {addressToBytes32} from "@layerzerolabs/lz-v2-utilities/contracts/LzLib.sol";

  contract MyOFT is OFT {
      constructor(string memory name, string memory symbol, address endpoint, address owner)
          OFT(name, symbol, endpoint, owner) {}

      function sendTokens(
          uint32 dstEid,
          address to,
          uint256 amount
      ) external {
          // Convert address to bytes32 for crosschain compatibility
          bytes32 toBytes32 = addressToBytes32(to);

          // Prepare the send parameters
          SendParam memory sendParam = SendParam({
              dstEid: dstEid,
              to: toBytes32,
              amountLD: amount,
              minAmountLD: amount,
              extraOptions: "0x",
              composeMsg: "0x",
              oftCmd: "0x"
          });

          // Send the tokens
          _send(sendParam, []);
      }
  }
  ```
</CodeGroup>

## Next Steps

* [**OFT Standard**](../../concepts/applications/oft-standard) - Learn about the OFT standard
* [**OFT Patterns & Extensions**](../../developers/evm/oft/oft-patterns-extensions) - Advanced OFT patterns
* [**Channel Security Model**](../../concepts/protocol/message-security#layerzero's-channel-security-model) - Understand security settings
* [**Default Configurations**](https://layerzeroscan.com/tools/defaults?version=V2) - View default configurations for all pathways
