frxUSD
Quickstart: Mint & Redeem frxUSD Cross-Chain

Mint & Redeem frxUSD cross-chain

This guide demonstrates how to use the viem framework in a simple script that enables a user to mint and redeem frxUSD with USDC cross-chain. To see a list of all supported networks, see our supported routes & networks page.

For this example, we will be doing the following:

  1. Deposit USDC on Ethereum mainnet to mint frxUSD on Base mainnet
  2. Redeem frxUSD on Ethereum mainnet to receive USDC on Base mainnet

Prerequisites

Before you start building the sample app to perform cross-chain frxUSD minting, ensure you have met the following prerequisites:

  1. Install Node.js and npm

    • Download and install Node.js directly or use a version manager like nvm.
    • npm is included with Node.js.
  2. Set up a non-custodial wallet (for example, MetaMask)

    • You can download, install, and create a MetaMask wallet from its official website.
    • During setup, create a wallet on Ethereum mainnet.
    • Retrieve the private key for your wallet, as it will be required in the script below.
  3. Fund your wallet with USDC, frxUSD, and the gas token on the chains you want to mint and redeem from

    • For this guide, we will be depositing USDC on Ethereum mainnet to mint frxUSD on Base mainnet and redeeming frxUSD on Ethereum mainnet to receive USDC on Base mainnet. Therefore, you will need to fund your wallet with USDC, frxUSD, and ETH on Ethereum mainnet and ETH on Base mainnet.

Project setup

To build the script, first set up your project environment and install the required dependencies.

1. Set up a new project

Create a new directory and initialize a new Node.js project with default settings:

mkdir frxusd-cross-chain-minting
cd frxusd-cross-chain-minting
npm init -y

This also creates a default package.json file.

2. Install dependencies

In your project directory, install the required dependencies, including viem:

npm install dotenv@^16.4.7 viem@^2.23.4

This sets up your development environment with the necessary libraries for building the script. It also updates the package.json file with the dependencies.

3. Add module type

Add "type": "module" to the package.json file:

{
  "name": "fraxnet-cross-chain-minting",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node mint-and-redeem-frxusd.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "dotenv": "^16.4.7",
    "viem": "^2.23.4"
  }
}

4. Configure environment variables

Create a .env file in your project directory and add your wallet private key:

echo "PRIVATE_KEY=your-private-key-here" > .env

Warning: This is strictly for testing purposes. Never share your private key.

Script setup

This section covers the necessary setup for the cross-chain minting script, including defining keys and addresses, and configuring the wallet and public clients for interacting with the source and destination chains.

1. Replace with your private key and wallet address

Ensure that this section of the file includes your private key for PRIVATE_KEY and associated wallet address for DESTINATION_ADDRESS.

The script also predefines the FraxNet factory contract address, the destination chain EID, the Circle's CCTP V2 information, the token addresses, the mint amount, and the redeem amount. These definitions are critical for successfully minting and redeeming frxUSD.

// ============ Configuration Constants ============
 
// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`, { nonceManager });
 
// Fraxnet Contract Addresses
const FRAXNET_DEPOSIT_FACTORY_ADDRESS = '0xA3D62f83C433e2A56Af392E08a705A52DEd63696'; // Fraxnet deposit factory address on Ethereum mainnet
 
// Destination Chain EID
const BASE_MAINNET_EID = 30184; // Base mainnet EID
 
// Circle's CCTP V2 Information
const ETHEREUM_SOURCE_DOMAIN_ID = 0; // Redeemed USDC is burned on Ethereum mainnet. Visit https://developers.circle.com/cctp/cctp-supported-blockchains for the domain IDs
const BASE_MESSAGE_TRANSMITTER_ADDRESS = '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64'; // Visit https://developers.circle.com/cctp/evm-smart-contracts#messagetransmitterv2-mainnet for the message transmitter address
 
// Token Addresses
const ETHEREUM_MAINNET_USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; // USDC address on Ethereum mainnet
const ETHEREUM_MAINNET_FRXUSD_ADDRESS = '0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29'; // frxUSD address on Ethereum mainnet
 
// Mint and Redeem Parameters
const MINT_AMOUNT = 10_000_000n; // 10 USDC (6 decimals) to be transferred to the Fraxnet contract to mint frxUSD
const REDEEM_AMOUNT = 10_000_000_000_000_000_000n; // 10 frxUSD (18 decimals) to be transferred to the Fraxnet contract to redeem for USDC
const DESTINATION_ADDRESS = 'enter-your-recipient-wallet-address-here'; // Recipient address
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(
  2,
)}`; // Convert to bytes32 format

2. Set up wallet and public clients

The wallet client and public client configure the appropriate network settings using viem. In this example, the script will be deploying Fraxnet contracts on Ethereum mainnet to deposit USDC to mint frxUSD on Base mainnet and redeem frxUSD to receive USDC on Base mainnet.

// Set up wallet and public clients
const ethereumWalletClient = createWalletClient({
  chain: mainnet,
  transport: http(),
  account,
});
 
const ethereumPublicClient = createPublicClient({
  chain: mainnet,
  transport: http(),
});
 
const baseWalletClient = createWalletClient({
  chain: base,
  transport: http(),
  account,
});
 
const basePublicClient = createPublicClient({
  chain: base,
  transport: http(),
});

Fraxnet Cross-Chain Mint & Redeem Process

The following sections outline the cross-chain mint & redeem process using the FraxNet system. Follow the steps below to perform cross-chain frxUSD mint & redeem:

1. Get FraxNet Contract Address

Get the FraxNet contract address for the specific recipient and destination chain from the FraxNet factory. The address is deterministic, so you can get it before the contract is deployed:

async function getFraxnetDepositAddress() {
  console.log('Getting Fraxnet deposit contract address...');
 
  const fraxnetDepositAddress = await ethereumPublicClient.readContract({
    address: FRAXNET_DEPOSIT_FACTORY_ADDRESS,
    abi: [
      {
        inputs: [
          { internalType: 'uint32', name: '_targetEid', type: 'uint32' },
          { internalType: 'bytes32', name: '_targetAddress', type: 'bytes32' },
          {
            internalType: 'bytes32',
            name: '_targetUsdcAtaAddress',
            type: 'bytes32',
          },
        ],
        name: 'getDeploymentAddress',
        outputs: [{ internalType: 'address', name: '', type: 'address' }],
        stateMutability: 'view',
        type: 'function',
      },
    ],
    functionName: 'getDeploymentAddress',
    args: [
      BASE_MAINNET_EID, // Destination chain EID
      DESTINATION_ADDRESS_BYTES32, // Recipient address
      zeroHash, // Recipient's USDC token account: only used for Solana, zero for every other chain
    ],
  });
 
  console.log(`FraxNet contract address: ${fraxnetDepositAddress}`);
  return fraxnetDepositAddress;
}

2. Deploy FraxNet Contract

Each FraxNet contract must be deployed via the FraxNet factory contract on Ethereum mainnet. This contract enables minting and redeeming of frxUSD on other chains.

Important: Each FraxNet contract deployed by the factory only has one recipient and one destination chain. Anyone can send USDC or frxUSD to the contract for minting or redeeming, but the actual minted frxUSD or redeemed USDC can only be sent to the address passed in when creating the FraxNet contract.

Anyone can deploy the FraxNet contract, so the function first checks whether it already exists:

async function deployFraxnetDepositContract(fraxnetDepositAddress) {
  // Anyone can deploy the FraxNet contract for a recipient, so it may already exist
  const code = await ethereumPublicClient.getCode({ address: fraxnetDepositAddress });
  if (code) {
    console.log('FraxNet contract is already deployed, skipping...');
    return;
  }
 
  console.log('Deploying FraxNet contract...');
  const txHash = await ethereumWalletClient.sendTransaction({
    to: FRAXNET_DEPOSIT_FACTORY_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [
            { internalType: 'uint32', name: '_targetEid', type: 'uint32' },
            {
              internalType: 'bytes32',
              name: '_targetAddress',
              type: 'bytes32',
            },
            {
              internalType: 'bytes32',
              name: '_targetUsdcAtaAddress',
              type: 'bytes32',
            },
          ],
          name: 'createFraxNetDeposit',
          outputs: [
            {
              internalType: 'contract FraxNetDeposit',
              name: 'newContract',
              type: 'address',
            },
          ],
          stateMutability: 'nonpayable',
          type: 'function',
        },
      ],
      functionName: 'createFraxNetDeposit',
      args: [
        BASE_MAINNET_EID, // Destination chain EID
        DESTINATION_ADDRESS_BYTES32, // Recipient address
        zeroHash, // Recipient's USDC token account: only used for Solana, zero for every other chain
      ],
    }),
  });
  await ethereumPublicClient.waitForTransactionReceipt({ hash: txHash });
 
  console.log(`Fraxnet contract deployment Tx: ${txHash}`);
  return txHash;
}

3. Transfer USDC to FraxNet Contract

Transfer USDC to the FraxNet contract to be used for minting frxUSD, and wait for the transfer to be confirmed:

async function transferUsdcToFraxnet(fraxnetDepositAddress) {
  console.log('Transferring USDC to Fraxnet contract...');
 
  const transferTx = await ethereumWalletClient.sendTransaction({
    to: ETHEREUM_MAINNET_USDC_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          type: 'function',
          name: 'transfer',
          stateMutability: 'nonpayable',
          inputs: [
            { name: 'to', type: 'address' },
            { name: 'amount', type: 'uint256' },
          ],
          outputs: [{ name: '', type: 'bool' }],
        },
      ],
      functionName: 'transfer',
      args: [fraxnetDepositAddress, MINT_AMOUNT],
    }),
  });
  const transferReceipt = await ethereumPublicClient.waitForTransactionReceipt({
    hash: transferTx,
  });
 
  console.log(`USDC Transfer Tx: ${transferTx}`);
  return transferReceipt;
}

4. Transfer frxUSD to FraxNet Contract

Transfer frxUSD to the FraxNet contract to be used for redeeming frxUSD for USDC, and wait for the transfer to be confirmed:

async function transferFrxUSDToFraxnet(fraxnetDepositAddress) {
  console.log('Transferring frxUSD to Fraxnet contract...');
 
  const transferTx = await ethereumWalletClient.sendTransaction({
    to: ETHEREUM_MAINNET_FRXUSD_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          type: 'function',
          name: 'transfer',
          stateMutability: 'nonpayable',
          inputs: [
            { name: 'to', type: 'address' },
            { name: 'amount', type: 'uint256' },
          ],
          outputs: [{ name: '', type: 'bool' }],
        },
      ],
      functionName: 'transfer',
      args: [fraxnetDepositAddress, REDEEM_AMOUNT],
    }),
  });
  const transferReceipt = await ethereumPublicClient.waitForTransactionReceipt({
    hash: transferTx,
  });
 
  console.log(`frxUSD Transfer Tx: ${transferTx}`);
  return transferReceipt;
}

5. Get Mint Quote

Calculate the FraxZero native fee required to process the frxUSD mint:

async function getMintQuote(fraxnetDepositAddress) {
  console.log('Getting mint quote...');
 
  const quoteResult = await ethereumPublicClient.readContract({
    address: fraxnetDepositAddress,
    abi: [
      {
        inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }],
        name: 'quote',
        outputs: [
          {
            components: [
              { internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
              { internalType: 'uint256', name: 'lzTokenFee', type: 'uint256' },
            ],
            internalType: 'struct IRemoteHop.MessagingFee',
            name: '',
            type: 'tuple',
          },
        ],
        stateMutability: 'view',
        type: 'function',
      },
    ],
    functionName: 'quote',
    args: [MINT_AMOUNT],
  });
 
  const nativeFee = quoteResult.nativeFee;
  console.log(`Native fee required: ${formatEther(nativeFee)} ETH`);
 
  return nativeFee;
}

6. Process USDC Deposit and Mint frxUSD to Base mainnet

Call processDeposit on the FraxNet contract to convert the deposited USDC to frxUSD 1:1 and send it to Base mainnet. Anyone can call processDeposit, and FraxNet's relayer also processes funded FraxNet contracts, so the function skips the call if the USDC has already been processed:

async function processDepositAndMintFrxUSD(fraxnetDepositAddress, nativeFee) {
  // Anyone can process the deposit, and FraxNet's relayer also processes funded FraxNet contracts
  const usdcBalance = await ethereumPublicClient.readContract({
    address: ETHEREUM_MAINNET_USDC_ADDRESS,
    abi: [
      {
        type: 'function',
        name: 'balanceOf',
        stateMutability: 'view',
        inputs: [{ name: 'account', type: 'address' }],
        outputs: [{ name: '', type: 'uint256' }],
      },
    ],
    functionName: 'balanceOf',
    args: [fraxnetDepositAddress],
  });
  if (usdcBalance < MINT_AMOUNT) {
    console.log('The USDC has already been processed, skipping...');
    return;
  }
 
  console.log('Processing deposit and minting frxUSD on Base mainnet...');
  const processMintTx = await ethereumWalletClient.sendTransaction({
    to: fraxnetDepositAddress,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }],
          name: 'processDeposit',
          outputs: [],
          stateMutability: 'payable',
          type: 'function',
        },
      ],
      functionName: 'processDeposit',
      args: [MINT_AMOUNT],
    }),
    value: nativeFee,
  });
  await ethereumPublicClient.waitForTransactionReceipt({ hash: processMintTx });
 
  console.log(`Process deposit Tx: ${processMintTx}`);
  return processMintTx;
}

7. Process the frxUSD Deposit and Redeem for USDC

Call processRedemption on the FraxNet contract to convert the deposited frxUSD to USDC 1:1. The FraxNet contract then burns the USDC on Ethereum mainnet via Circle's CCTP V2 so it can be minted on Base mainnet. If the redemption has already been processed, the function returns the transaction that processed it:

async function processRedemptionAndRedeemForUsdc(fraxnetDepositAddress, fromBlock) {
  // Anyone can process the redemption, and FraxNet's relayer also processes funded FraxNet contracts
  const frxUsdBalance = await ethereumPublicClient.readContract({
    address: ETHEREUM_MAINNET_FRXUSD_ADDRESS,
    abi: [
      {
        type: 'function',
        name: 'balanceOf',
        stateMutability: 'view',
        inputs: [{ name: 'account', type: 'address' }],
        outputs: [{ name: '', type: 'uint256' }],
      },
    ],
    functionName: 'balanceOf',
    args: [fraxnetDepositAddress],
  });
  if (frxUsdBalance < REDEEM_AMOUNT) {
    // Use the transaction that already processed the redemption
    const [redemption] = await ethereumPublicClient.getLogs({
      address: fraxnetDepositAddress,
      event: parseAbiItem(
        'event ProcessedRedemption(uint32 indexed targetEid, bytes32 indexed targetAddress, uint256 frxUSDIn, uint256 usdcOut)',
      ),
      fromBlock,
    });
    if (!redemption) {
      throw new Error('The Fraxnet contract holds less frxUSD than REDEEM_AMOUNT');
    }
    console.log(`The frxUSD has already been redeemed in Tx: ${redemption.transactionHash}`);
    return redemption.transactionHash;
  }
 
  console.log('Processing redemption...');
  const processRedemptionTx = await ethereumWalletClient.sendTransaction({
    to: fraxnetDepositAddress,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }],
          name: 'processRedemption',
          outputs: [],
          stateMutability: 'nonpayable',
          type: 'function',
        },
      ],
      functionName: 'processRedemption',
      args: [REDEEM_AMOUNT],
    }),
  });
  await ethereumPublicClient.waitForTransactionReceipt({ hash: processRedemptionTx });
 
  console.log(`Process redemption Tx: ${processRedemptionTx}`);
  return processRedemptionTx;
}

8. Receive USDC on Base mainnet via Circle's CCTP V2

The USDC is burned on Ethereum mainnet, so the function looks up Circle's attestation under Ethereum's source domain (0). For a standard transfer, Circle attests after Ethereum finality, which takes about 15–20 minutes, so the function checks every 30 seconds until the attestation is ready. It then calls receiveMessage on Base mainnet to mint the USDC to the recipient.

Note: Anyone can call receiveMessage, so the function first checks usedNonces in case it has already been called.

async function completeCrossChainTransferOfRedeemedUsdcToBaseMainnet(processRedemptionTx) {
  // The USDC is burned on Ethereum mainnet, so the message is looked up under Ethereum's source domain.
  // Circle attests a standard transfer after Ethereum finality, which takes about 15-20 minutes.
  console.log("Waiting for Circle's attestation...");
  let message;
  while (true) {
    const response = await fetch(
      `https://iris-api.circle.com/v2/messages/${ETHEREUM_SOURCE_DOMAIN_ID}?transactionHash=${processRedemptionTx}`,
    );
    message = response.ok ? (await response.json())?.messages?.[0] : undefined;
    if (message?.status === 'complete') break;
    console.log('Attestation is not ready yet, checking again in 30 seconds...');
    await new Promise((resolve) => setTimeout(resolve, 30_000));
  }
 
  // Anyone can call receiveMessage, so check whether it has already been called
  const usedNonces = await basePublicClient.readContract({
    address: BASE_MESSAGE_TRANSMITTER_ADDRESS,
    abi: [
      {
        inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
        name: 'usedNonces',
        outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
        stateMutability: 'view',
        type: 'function',
      },
    ],
    functionName: 'usedNonces',
    args: [message.eventNonce],
  });
  if (usedNonces !== 0n) {
    console.log('receiveMessage has already been called, skipping...');
    return;
  }
 
  console.log('Calling receiveMessage on Base mainnet...');
  const receiveMessageTx = await baseWalletClient.sendTransaction({
    to: BASE_MESSAGE_TRANSMITTER_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [
            {
              internalType: 'bytes',
              name: 'message',
              type: 'bytes',
            },
            {
              internalType: 'bytes',
              name: 'attestation',
              type: 'bytes',
            },
          ],
          name: 'receiveMessage',
          outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }],
          stateMutability: 'nonpayable',
          type: 'function',
        },
      ],
      functionName: 'receiveMessage',
      args: [message.message, message.attestation],
    }),
  });
  await basePublicClient.waitForTransactionReceipt({ hash: receiveMessageTx });
 
  console.log(`USDC transferred to Base mainnet via Circle's CCTP V2: ${receiveMessageTx}`);
  return receiveMessageTx;
}

Build the script

Create a mint-and-redeem-frxusd.js file in your project directory and paste the following complete script:

mint-and-redeem-frxusd.js

// ============ Setup (imports, account, clients) ============
import 'dotenv/config';
import {
  createWalletClient,
  createPublicClient,
  http,
  zeroHash,
  encodeFunctionData,
  formatEther,
  parseAbiItem,
  nonceManager,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet, base } from 'viem/chains';
 
// ============ Configuration Constants ============
 
// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`, { nonceManager });
 
// Fraxnet Contract Addresses
const FRAXNET_DEPOSIT_FACTORY_ADDRESS = '0xA3D62f83C433e2A56Af392E08a705A52DEd63696'; // Fraxnet deposit factory address on Ethereum mainnet
 
// Destination Chain EID
const BASE_MAINNET_EID = 30184; // Base mainnet EID
 
// Circle's CCTP V2 Information
const ETHEREUM_SOURCE_DOMAIN_ID = 0; // Redeemed USDC is burned on Ethereum mainnet. Visit https://developers.circle.com/cctp/cctp-supported-blockchains for the domain IDs
const BASE_MESSAGE_TRANSMITTER_ADDRESS = '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64'; // Visit https://developers.circle.com/cctp/evm-smart-contracts#messagetransmitterv2-mainnet for the message transmitter address
 
// Token Addresses
const ETHEREUM_MAINNET_USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; // USDC address on Ethereum mainnet
const ETHEREUM_MAINNET_FRXUSD_ADDRESS = '0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29'; // frxUSD address on Ethereum mainnet
 
// Mint and Redeem Parameters
const MINT_AMOUNT = 10_000_000n; // 10 USDC (6 decimals) to be transferred to the Fraxnet contract to mint frxUSD
const REDEEM_AMOUNT = 10_000_000_000_000_000_000n; // 10 frxUSD (18 decimals) to be transferred to the Fraxnet contract to redeem for USDC
const DESTINATION_ADDRESS = 'enter-your-recipient-wallet-address-here'; // Recipient address
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(
  2,
)}`; // Convert to bytes32 format
 
// Wallet and public clients
const ethereumWalletClient = createWalletClient({
  chain: mainnet,
  transport: http(),
  account,
});
 
const ethereumPublicClient = createPublicClient({
  chain: mainnet,
  transport: http(),
});
 
const baseWalletClient = createWalletClient({
  chain: base,
  transport: http(),
  account,
});
 
const basePublicClient = createPublicClient({
  chain: base,
  transport: http(),
});
 
// 1) Get the Fraxnet contract address, which is known before the contract is deployed
async function getFraxnetDepositAddress() {
  console.log('Getting Fraxnet deposit contract address...');
 
  const fraxnetDepositAddress = await ethereumPublicClient.readContract({
    address: FRAXNET_DEPOSIT_FACTORY_ADDRESS,
    abi: [
      {
        inputs: [
          { internalType: 'uint32', name: '_targetEid', type: 'uint32' },
          { internalType: 'bytes32', name: '_targetAddress', type: 'bytes32' },
          {
            internalType: 'bytes32',
            name: '_targetUsdcAtaAddress',
            type: 'bytes32',
          },
        ],
        name: 'getDeploymentAddress',
        outputs: [{ internalType: 'address', name: '', type: 'address' }],
        stateMutability: 'view',
        type: 'function',
      },
    ],
    functionName: 'getDeploymentAddress',
    args: [
      BASE_MAINNET_EID, // Destination chain EID
      DESTINATION_ADDRESS_BYTES32, // Recipient address
      zeroHash, // Recipient's USDC token account: only used for Solana, zero for every other chain
    ],
  });
 
  console.log(`FraxNet contract address: ${fraxnetDepositAddress}`);
  return fraxnetDepositAddress;
}
 
// 2) Deploy the FraxNet contract to allow for the minting and redemption of frxUSD
async function deployFraxnetDepositContract(fraxnetDepositAddress) {
  // Anyone can deploy the FraxNet contract for a recipient, so it may already exist
  const code = await ethereumPublicClient.getCode({ address: fraxnetDepositAddress });
  if (code) {
    console.log('FraxNet contract is already deployed, skipping...');
    return;
  }
 
  console.log('Deploying FraxNet contract...');
  const txHash = await ethereumWalletClient.sendTransaction({
    to: FRAXNET_DEPOSIT_FACTORY_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [
            { internalType: 'uint32', name: '_targetEid', type: 'uint32' },
            {
              internalType: 'bytes32',
              name: '_targetAddress',
              type: 'bytes32',
            },
            {
              internalType: 'bytes32',
              name: '_targetUsdcAtaAddress',
              type: 'bytes32',
            },
          ],
          name: 'createFraxNetDeposit',
          outputs: [
            {
              internalType: 'contract FraxNetDeposit',
              name: 'newContract',
              type: 'address',
            },
          ],
          stateMutability: 'nonpayable',
          type: 'function',
        },
      ],
      functionName: 'createFraxNetDeposit',
      args: [
        BASE_MAINNET_EID, // Destination chain EID
        DESTINATION_ADDRESS_BYTES32, // Recipient address
        zeroHash, // Recipient's USDC token account: only used for Solana, zero for every other chain
      ],
    }),
  });
  await ethereumPublicClient.waitForTransactionReceipt({ hash: txHash });
 
  console.log(`Fraxnet contract deployment Tx: ${txHash}`);
  return txHash;
}
 
// 3) Transfer USDC to the FraxNet contract to allow for the minting of frxUSD on Base mainnet
async function transferUsdcToFraxnet(fraxnetDepositAddress) {
  console.log('Transferring USDC to Fraxnet contract...');
 
  const transferTx = await ethereumWalletClient.sendTransaction({
    to: ETHEREUM_MAINNET_USDC_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          type: 'function',
          name: 'transfer',
          stateMutability: 'nonpayable',
          inputs: [
            { name: 'to', type: 'address' },
            { name: 'amount', type: 'uint256' },
          ],
          outputs: [{ name: '', type: 'bool' }],
        },
      ],
      functionName: 'transfer',
      args: [fraxnetDepositAddress, MINT_AMOUNT],
    }),
  });
  const transferReceipt = await ethereumPublicClient.waitForTransactionReceipt({
    hash: transferTx,
  });
 
  console.log(`USDC Transfer Tx: ${transferTx}`);
  return transferReceipt;
}
 
// 4) Transfer frxUSD to the FraxNet contract to allow for the redemption of frxUSD for USDC on Base mainnet
async function transferFrxUSDToFraxnet(fraxnetDepositAddress) {
  console.log('Transferring frxUSD to Fraxnet contract...');
 
  const transferTx = await ethereumWalletClient.sendTransaction({
    to: ETHEREUM_MAINNET_FRXUSD_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          type: 'function',
          name: 'transfer',
          stateMutability: 'nonpayable',
          inputs: [
            { name: 'to', type: 'address' },
            { name: 'amount', type: 'uint256' },
          ],
          outputs: [{ name: '', type: 'bool' }],
        },
      ],
      functionName: 'transfer',
      args: [fraxnetDepositAddress, REDEEM_AMOUNT],
    }),
  });
  const transferReceipt = await ethereumPublicClient.waitForTransactionReceipt({
    hash: transferTx,
  });
 
  console.log(`frxUSD Transfer Tx: ${transferTx}`);
  return transferReceipt;
}
 
// 5) Get mint quote
async function getMintQuote(fraxnetDepositAddress) {
  console.log('Getting mint quote...');
 
  const quoteResult = await ethereumPublicClient.readContract({
    address: fraxnetDepositAddress,
    abi: [
      {
        inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }],
        name: 'quote',
        outputs: [
          {
            components: [
              { internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
              { internalType: 'uint256', name: 'lzTokenFee', type: 'uint256' },
            ],
            internalType: 'struct IRemoteHop.MessagingFee',
            name: '',
            type: 'tuple',
          },
        ],
        stateMutability: 'view',
        type: 'function',
      },
    ],
    functionName: 'quote',
    args: [MINT_AMOUNT],
  });
 
  const nativeFee = quoteResult.nativeFee;
  console.log(`Native fee required: ${formatEther(nativeFee)} ETH`);
 
  return nativeFee;
}
 
// 6) Process deposit and mint frxUSD on Base mainnet
async function processDepositAndMintFrxUSD(fraxnetDepositAddress, nativeFee) {
  // Anyone can process the deposit, and FraxNet's relayer also processes funded FraxNet contracts
  const usdcBalance = await ethereumPublicClient.readContract({
    address: ETHEREUM_MAINNET_USDC_ADDRESS,
    abi: [
      {
        type: 'function',
        name: 'balanceOf',
        stateMutability: 'view',
        inputs: [{ name: 'account', type: 'address' }],
        outputs: [{ name: '', type: 'uint256' }],
      },
    ],
    functionName: 'balanceOf',
    args: [fraxnetDepositAddress],
  });
  if (usdcBalance < MINT_AMOUNT) {
    console.log('The USDC has already been processed, skipping...');
    return;
  }
 
  console.log('Processing deposit and minting frxUSD on Base mainnet...');
  const processMintTx = await ethereumWalletClient.sendTransaction({
    to: fraxnetDepositAddress,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }],
          name: 'processDeposit',
          outputs: [],
          stateMutability: 'payable',
          type: 'function',
        },
      ],
      functionName: 'processDeposit',
      args: [MINT_AMOUNT],
    }),
    value: nativeFee,
  });
  await ethereumPublicClient.waitForTransactionReceipt({ hash: processMintTx });
 
  console.log(`Process deposit Tx: ${processMintTx}`);
  return processMintTx;
}
 
// 7) Process redemption and redeem frxUSD for USDC on Base mainnet
async function processRedemptionAndRedeemForUsdc(fraxnetDepositAddress, fromBlock) {
  // Anyone can process the redemption, and FraxNet's relayer also processes funded FraxNet contracts
  const frxUsdBalance = await ethereumPublicClient.readContract({
    address: ETHEREUM_MAINNET_FRXUSD_ADDRESS,
    abi: [
      {
        type: 'function',
        name: 'balanceOf',
        stateMutability: 'view',
        inputs: [{ name: 'account', type: 'address' }],
        outputs: [{ name: '', type: 'uint256' }],
      },
    ],
    functionName: 'balanceOf',
    args: [fraxnetDepositAddress],
  });
  if (frxUsdBalance < REDEEM_AMOUNT) {
    // Use the transaction that already processed the redemption
    const [redemption] = await ethereumPublicClient.getLogs({
      address: fraxnetDepositAddress,
      event: parseAbiItem(
        'event ProcessedRedemption(uint32 indexed targetEid, bytes32 indexed targetAddress, uint256 frxUSDIn, uint256 usdcOut)',
      ),
      fromBlock,
    });
    if (!redemption) {
      throw new Error('The Fraxnet contract holds less frxUSD than REDEEM_AMOUNT');
    }
    console.log(`The frxUSD has already been redeemed in Tx: ${redemption.transactionHash}`);
    return redemption.transactionHash;
  }
 
  console.log('Processing redemption...');
  const processRedemptionTx = await ethereumWalletClient.sendTransaction({
    to: fraxnetDepositAddress,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }],
          name: 'processRedemption',
          outputs: [],
          stateMutability: 'nonpayable',
          type: 'function',
        },
      ],
      functionName: 'processRedemption',
      args: [REDEEM_AMOUNT],
    }),
  });
  await ethereumPublicClient.waitForTransactionReceipt({ hash: processRedemptionTx });
 
  console.log(`Process redemption Tx: ${processRedemptionTx}`);
  return processRedemptionTx;
}
 
// 8) Complete the cross-chain transfer of the redeemed USDC to Base mainnet via Circle's CCTP V2
async function completeCrossChainTransferOfRedeemedUsdcToBaseMainnet(processRedemptionTx) {
  // The USDC is burned on Ethereum mainnet, so the message is looked up under Ethereum's source domain.
  // Circle attests a standard transfer after Ethereum finality, which takes about 15-20 minutes.
  console.log("Waiting for Circle's attestation...");
  let message;
  while (true) {
    const response = await fetch(
      `https://iris-api.circle.com/v2/messages/${ETHEREUM_SOURCE_DOMAIN_ID}?transactionHash=${processRedemptionTx}`,
    );
    message = response.ok ? (await response.json())?.messages?.[0] : undefined;
    if (message?.status === 'complete') break;
    console.log('Attestation is not ready yet, checking again in 30 seconds...');
    await new Promise((resolve) => setTimeout(resolve, 30_000));
  }
 
  // Anyone can call receiveMessage, so check whether it has already been called
  const usedNonces = await basePublicClient.readContract({
    address: BASE_MESSAGE_TRANSMITTER_ADDRESS,
    abi: [
      {
        inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
        name: 'usedNonces',
        outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
        stateMutability: 'view',
        type: 'function',
      },
    ],
    functionName: 'usedNonces',
    args: [message.eventNonce],
  });
  if (usedNonces !== 0n) {
    console.log('receiveMessage has already been called, skipping...');
    return;
  }
 
  console.log('Calling receiveMessage on Base mainnet...');
  const receiveMessageTx = await baseWalletClient.sendTransaction({
    to: BASE_MESSAGE_TRANSMITTER_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [
            {
              internalType: 'bytes',
              name: 'message',
              type: 'bytes',
            },
            {
              internalType: 'bytes',
              name: 'attestation',
              type: 'bytes',
            },
          ],
          name: 'receiveMessage',
          outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }],
          stateMutability: 'nonpayable',
          type: 'function',
        },
      ],
      functionName: 'receiveMessage',
      args: [message.message, message.attestation],
    }),
  });
  await basePublicClient.waitForTransactionReceipt({ hash: receiveMessageTx });
 
  console.log(`USDC transferred to Base mainnet via Circle's CCTP V2: ${receiveMessageTx}`);
  return receiveMessageTx;
}
 
async function main() {
  if (DESTINATION_ADDRESS === 'enter-your-recipient-wallet-address-here') {
    console.error(
      'Destination address is not set. Please set the DESTINATION_ADDRESS variable in the mint-and-redeem-frxusd.js file.',
    );
    process.exit(1);
  }
  const command = process.argv[2]; // Get command from CLI
 
  switch (command) {
    case 'deploy-fraxnet-deposit-contract': {
      const fraxnetDepositAddress = await getFraxnetDepositAddress();
      await deployFraxnetDepositContract(fraxnetDepositAddress);
      break;
    }
 
    case 'mint-frxusd': {
      console.log(
        'Depositing USDC on Ethereum mainnet to mint frxUSD on Base mainnet...',
      );
      const fraxnetDepositAddressForMint = await getFraxnetDepositAddress();
      await deployFraxnetDepositContract(fraxnetDepositAddressForMint);
      await transferUsdcToFraxnet(fraxnetDepositAddressForMint);
      const nativeFee = await getMintQuote(fraxnetDepositAddressForMint);
      await processDepositAndMintFrxUSD(fraxnetDepositAddressForMint, nativeFee);
      break;
    }
 
    case 'redeem-frxusd': {
      console.log(
        'Depositing frxUSD on Ethereum mainnet to redeem USDC on Base mainnet...',
      );
      const fraxnetDepositAddressForRedeem = await getFraxnetDepositAddress();
      await deployFraxnetDepositContract(fraxnetDepositAddressForRedeem);
      const transferReceipt = await transferFrxUSDToFraxnet(fraxnetDepositAddressForRedeem);
      const processRedemptionTx = await processRedemptionAndRedeemForUsdc(
        fraxnetDepositAddressForRedeem,
        transferReceipt.blockNumber,
      );
      await completeCrossChainTransferOfRedeemedUsdcToBaseMainnet(processRedemptionTx);
      break;
    }
 
    default:
      console.error(
        'Invalid command. Please use "deploy-fraxnet-deposit-contract", "mint-frxusd" or "redeem-frxusd".',
      );
      process.exit(1);
  }
 
  console.log('Done!');
}
 
main().catch(console.error);

Test the script

To deploy the FraxNet contract on its own, run the following command. The mint and redeem commands also deploy it if it does not exist yet:

node mint-and-redeem-frxusd.js deploy-fraxnet-deposit-contract

To test minting frxUSD, run the following command. The minted frxUSD reaches Base mainnet once the LayerZero transfer is delivered, which you can follow on LayerZero Scan (opens in a new tab):

node mint-and-redeem-frxusd.js mint-frxusd

To test redeeming frxUSD, run the following command. It waits for Circle's attestation before receiving the USDC on Base mainnet, which takes about 15–20 minutes:

node mint-and-redeem-frxusd.js redeem-frxusd

Each command logs its transaction hashes once the transactions are confirmed.

What's next