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

# Shimmer Mainnet OFT Quickstart

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

Welcome! In this guide you'll mint and transfer a lightweight **Omnichain Fungible Token (OFT)** between **Shimmer Mainnet** 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
  // Shimmer Mainnet (EID=30230)
  'shimmer-mainnet': {
    eid: EndpointId.SHIMMER_V2_MAINNET,
    url: process.env.RPC_URL_SHIMMER || 'https://json-rpc.evm.shimmer.network',
    accounts,
  },
  // another network you want to connect to
  'optimism-mainnet': {
    eid: EndpointId.OPTIMISM_V2_MAINNET,
    url: process.env.RPC_URL_OPTIMISM || 'https://mainnet.optimism.io',
    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 shimmerContract: OmniPointHardhat = {
  eid: EndpointId.SHIMMER_V2_MAINNET,
  contractName: 'MyOFT',
};

const optimismContract: OmniPointHardhat = {
  eid: EndpointId.OPTIMISM_V2_MAINNET,
  contractName: 'MyOFT',
};

// To connect all the above chains to each other, we need the following pathways:
// Optimism <-> shimmer
// shimmer    <-> Optimism

// 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. Optimism)
    optimismContract,

    // 2) Chain A's contract (e.g. shimmer)
    shimmerContract,

    // 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 Optimism → shimmer, confirmations for shimmer → Optimism]
    [1, 1],

    // 5) Enforced execution options:
    //    [options for Optimism → shimmer, options for shimmer → Optimism]
    [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: optimismContract}, {contract: shimmerContract}],
    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 shimmer
```

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 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)
    .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 decimals = await oftContract.decimals();
      const amount = ethers.utils.parseUnits(taskArgs.amount, decimals);
      let 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
      };
      // Get the quote for the send operation
      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. See: https://layerzeroscan.com/tx/${r.hash}`);
    });
  ```

  ```solidity wrap Foundry Script theme={null}
  // SPDX-License-Identifier: UNLICENSED
  pragma solidity ^0.8.13;

  import {Script, console} from "forge-std/Script.sol";

  import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol";
  import { SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
  import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
  import { MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
  import { MyOFT } from "../contracts/MyOFT.sol";

  contract SendOFT is Script {
      using OptionsBuilder for bytes;

      /**
       * @dev Converts an address to bytes32.
       * @param _addr The address to convert.
       * @return The bytes32 representation of the address.
       */
      function addressToBytes32(address _addr) internal pure returns (bytes32) {
          return bytes32(uint256(uint160(_addr)));
      }

      function run() public {
          // Fetching environment variables
          address oftAddress = vm.envAddress("OFT_ADDRESS");
          address toAddress = vm.envAddress("TO_ADDRESS");
          uint256 _tokensToSend = vm.envUint("TOKENS_TO_SEND");

          // Fetch the private key from environment variable
          uint256 privateKey = vm.envUint("PRIVATE_KEY");

          // Start broadcasting with the private key
          vm.startBroadcast(privateKey);

          MyOFT sourceOFT = MyOFT(oftAddress);

          bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(65000, 0);
          SendParam memory sendParam = SendParam(
              30111, // You can also make this dynamic if needed
              addressToBytes32(toAddress),
              _tokensToSend,
              _tokensToSend * 9 / 10,
              _extraOptions,
              "",
              ""
          );

          MessagingFee memory fee = sourceOFT.quoteSend(sendParam, false);

          console.log("Fee amount: ", fee.nativeFee);

          sourceOFT.send{value: fee.nativeFee}(sendParam, fee, msg.sender);

          // Stop broadcasting
          vm.stopBroadcast();
      }
  }
  ```
</CodeGroup>

## Done!

You've issued an omnichain token and bridged it from **Shimmer Mainnet** to Optimism. Customize supply logic, fees, or add more chains by applying changes to the core contract, redeploying, and repeating the wiring step.

## Troubleshooting

If your `quoteSend` call reverts, it usually means that your LayerZero wiring hasn't been fully configured or there's no default pathway for the chains you're trying to bridge. Here's how to diagnose and fix it:

1. **Wiring Didn't Succeed**\
   Run the following to inspect your on‑chain wiring configuration:

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

   See that your source configuration has a valid send library, DVN address, and target eid.

2. **No Default Pathway**\
   LayerZero default settings should be considered placeholders. Sometimes the LayerZero defaults will contain a `LzDeadDVN`. Those entries indicate that a default pathway setting does not exist.

   * **Check:** You can see if your configuration contains a LzDeadDVN by viewing the [Default Config Checker](https://layerzeroscan.com/tools/defaults?srcChainKey\[0]=shimmer\&version=V2\&dstChainKey\[0]=optimism) on LayerZero Scan.

   * **Fix**: Open your `layerzero.config.ts` and under the relevant `pathways` entry, add working DVN providers (in the `[ requiredDVN[], [ optionalDVN[], threshold ] ]` section).

   * Re-run your wiring command for the connections so that the wiring on both chains is live.
     ```ts wrap theme={null}
     [
       optimismContract,
       currentContract,
       // Replace <SECONDARY_DVN> with a non-LayerZero-Labs DVN provider for this pathway.
       // See https://docs.layerzero.network/v2/deployments/dvn-addresses?chains=shimmer for available providers.
       [['LayerZero Labs', '<SECONDARY_DVN>'], []], // required & optional DVNs
       [1, 1],                                       // required block confirmations
       [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS],
     ],
     ```

   <Warning>
     Production deployments should use multiple required DVNs from independent operators. A single-DVN configuration means a compromise of that one verifier results in unrestricted forged messages on the pathway. See the [Integration Checklist](../../tools/integration-checklist#set-security-and-executor-configurations-on-every-pathway).
   </Warning>

Once you've updated your config, retry your `quoteSend` flow. It should now return a fee estimate instead of reverting.

## Next steps

* See the [Configuring Pathways](../../get-started/create-lz-oapp/configuring-pathways) section to learn more about managing your OFTs across the entire network mesh.

* See the [Available DVNs](../dvn-addresses.md?chains=shimmer) and [Executor](../deployed-contracts.md?chains=shimmer) to configure between.

* Learn how the protocol works by reading the [Core Concepts](../../concepts/getting-started/what-is-layerzero) section.

* Learn more about how the OFT contract works on the EVM by reading the [OFT Quickstart](../../developers/evm/oft/quickstart) in the EVM section.
