frxUSD
Quickstart: Stake & Unstake frxUSD on EVM chains

Stake & Unstake frxUSD on EVM chains

This guide shows how to use the viem framework to stake frxUSD for sfrxUSD and unstake sfrxUSD back to frxUSD on all EVM chains (excluding Ethereum mainnet and Fraxtal mainnet). We'll use Arbitrum mainnet as our example.

To see a list of all supported networks, see the Stake & Unstake Supported Networks and EIDs page.

Note: The methods for staking and unstaking frxUSD on Ethereum mainnet and Fraxtal mainnet are different from the method for staking and unstaking frxUSD on all other EVM chains. See the Stake & Unstake frxUSD on Fraxtal guide or the Stake & Unstake frxUSD on Ethereum guide for more information.

Prerequisites

Before you start building the sample app to perform frxUSD staking operations, 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 Arbitrum mainnet.
    • Retrieve the private key for your wallet, as it will be required in the script below.
  3. Fund your wallet with the gas token on the chain you want to stake from

    • For this guide, we will be staking and unstaking frxUSD on Arbitrum mainnet. Therefore, you will need to fund your wallet with ETH on Arbitrum mainnet.
  4. Fund your wallet with frxUSD and sfrxUSD on the chain you want to stake and unstake from

    • For this guide, we will be staking and unstaking frxUSD on Arbitrum mainnet. Therefore, you will need to fund your wallet with frxUSD and sfrxUSD on Arbitrum 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-staking-arbitrum
cd frxusd-staking-arbitrum
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": "frxusd-staking-arbitrum",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node stake-and-unstake.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 staking script, including defining contract addresses for Arbitrum mainnet and configuring the wallet client.

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 Hop contract address, the frxUSD contract address, the sfrxUSD contract address, the staking amount, and the unstaking amount. These definitions are critical for successfully staking and unstaking frxUSD.

// ============ Configuration Constants ============
 
// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`, { nonceManager });
 
// FILL IN THE FOLLOWING VARIABLE
const DESTINATION_ADDRESS = 'enter-your-recipient-wallet-address-here'; // Recipient address
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(2)}`; // Convert to bytes32 format
 
// Hop Contract Address
const ARBITRUM_MAINNET_HOP_ADDRESS = '0xa46A266dCBf199a71532c76967e200994C5A0D6d';
 
// frxUSD and sfrxUSD Contract Addresses
const ARBITRUM_MAINNET_FRXUSD_ADDRESS = '0x80Eede496655FB9047dd39d9f418d5483ED600df';
const ARBITRUM_MAINNET_SFRXUSD_ADDRESS = '0x5Bff88cA1442c2496f7E475E9e7786383Bc070c0';
 
// Staking Parameters
const STAKE_AMOUNT = 10_000_000_000_000_000_000n; // 10 frxUSD (18 decimals)
const UNSTAKE_AMOUNT = 10_000_000_000_000_000_000n; // 10 sfrxUSD (18 decimals)

2. Set up clients

The script creates a wallet client and a public client for Arbitrum network:

// Set up wallet client
const arbitrumWalletClient = createWalletClient({
  chain: arbitrum,
  transport: http(),
  account,
});
 
// Set up public client
const arbitrumPublicClient = createPublicClient({
  chain: arbitrum,
  transport: http(),
});

frxUSD staking and unstaking process

The following sections outline the relevant staking and unstaking logic of the sample script. In this example, we are first staking 1 frxUSD on Arbitrum mainnet to receive 1 sfrxUSD. Then we are unstaking 1 sfrxUSD on Arbitrum mainnet to receive 1 frxUSD back. Follow the steps below to perform the staking and unstaking:

1. Approve frxUSD and sfrxUSD

Before staking or unstaking, approve frxUSD for the Hop contract so it can pull frxUSD and sfrxUSD from your wallet:

async function approveForStakingOrUnstaking(tokenAddress, amount) {
  console.log(`Approving frxUSD for staking on Arbitrum mainnet...`);
  const approveTx = await arbitrumWalletClient.sendTransaction({
    to: tokenAddress,
    data: encodeFunctionData({
      abi: [
        {
          type: 'function',
          name: 'approve',
          stateMutability: 'nonpayable',
          inputs: [
            { name: 'spender', type: 'address' },
            { name: 'amount', type: 'uint256' },
          ],
          outputs: [{ name: '', type: 'bool' }],
        },
      ],
      functionName: 'approve',
      args: [ARBITRUM_MAINNET_HOP_ADDRESS, amount],
    }),
  });
  console.log(`Arbitrum mainnet frxUSD Approval Tx: ${approveTx}`);
}
 
async function approveFrxusdForStaking() {
  await approveForStakingOrUnstaking(ARBITRUM_MAINNET_FRXUSD_ADDRESS, STAKE_AMOUNT);
}
 
async function approveSfrxusdForUnstaking() {
  await approveForStakingOrUnstaking(ARBITRUM_MAINNET_SFRXUSD_ADDRESS, UNSTAKE_AMOUNT);
}

2. Retrieve FraxZero Quotes

In this step, you call the quote function on the RemoteHop contract deployed on Arbitrum mainnet to get the native fee amounts required for the staking and unstaking operations. You specify the following parameters:

  • OFT: The contract address of the frxUSD token being staked or the sfrxUSD token being unstaked
  • Destination address: The wallet address that will receive the frxUSD or sfrxUSD (in bytes32 format)
  • Amount: The amount of frxUSD to stake or the amount of sfrxUSD to unstake (in wei)

The quote function returns the native fee amount required for the staking and unstaking operations.

async function retrieveQuote(tokenAddress, amount) {
  console.log('Retrieving quote...');
  const quote = await arbitrumPublicClient.readContract({
    address: ARBITRUM_MAINNET_HOP_ADDRESS,
    abi: [
      {
        inputs: [
          { internalType: 'address', name: '_oft', type: 'address' },
          { internalType: 'bytes32', name: '_to', type: 'bytes32' },
          { internalType: 'uint256', name: '_amountLD', type: 'uint256' },
        ],
        name: 'quote',
        outputs: [
          {
            components: [
              { internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
              { internalType: 'uint256', name: 'lzTokenFee', type: 'uint256' },
            ],
            internalType: 'struct MessagingFee',
            name: 'fee',
            type: 'tuple',
          },
        ],
        stateMutability: 'view',
        type: 'function',
      },
    ],
    functionName: 'quote',
    args: [
      tokenAddress,
      DESTINATION_ADDRESS_BYTES32, // Use bytes32 format
      amount,
    ],
  });
  const nativeFee = quote.nativeFee;
  return nativeFee;
}

3. Stake and unstake frxUSD

After approving frxUSD and retrieving the FraxZero quote, stake or unstake by calling the Hop contract's mintRedeem function to receive sfrxUSD or frxUSD back:

async function stakeOrUnstakeFrxusd(tokenAddress, amount) {
  console.log(`Staking frxUSD for sfrxUSD on Arbitrum mainnet...`);
  const nativeFee = await retrieveQuote(tokenAddress, amount);
  const txHash = await arbitrumWalletClient.sendTransaction({
    to: ARBITRUM_MAINNET_HOP_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [
            { internalType: 'address', name: '_oft', type: 'address' },
            { internalType: 'uint256', name: '_amountLD', type: 'uint256' },
          ],
          name: 'mintRedeem',
          outputs: [],
          stateMutability: 'payable',
          type: 'function',
        },
      ],
      functionName: 'mintRedeem',
      args: [tokenAddress, amount],
    }),
    value: nativeFee,
  });
  console.log(`Arbitrum mainnet Stake Tx: ${txHash}`);
}
 
async function stakeFrxusd() {
  await stakeOrUnstakeFrxusd(ARBITRUM_MAINNET_FRXUSD_ADDRESS, STAKE_AMOUNT);
}
 
async function unstakeFrxusd() {
  await stakeOrUnstakeFrxusd(ARBITRUM_MAINNET_SFRXUSD_ADDRESS, UNSTAKE_AMOUNT);
}

Build the script

Create a stake-and-unstake.js file in your project directory and paste the following complete script:

stake-and-unstake.js

import 'dotenv/config';
import {
  createWalletClient,
  http,
  encodeFunctionData,
  nonceManager,
  createPublicClient,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrum } from 'viem/chains';
 
// ============ Configuration Constants ============
 
// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`, { nonceManager });
 
// FILL IN THE FOLLOWING VARIABLE
const DESTINATION_ADDRESS = 'enter-your-recipient-wallet-address-here'; // Recipient address
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(
  2,
)}`; // Convert to bytes32 format
 
// Hop Contract Address
const ARBITRUM_MAINNET_HOP_ADDRESS = '0xa46A266dCBf199a71532c76967e200994C5A0D6d';
 
// frxUSD and sfrxUSD Contract Addresses
const ARBITRUM_MAINNET_FRXUSD_ADDRESS = '0x80Eede496655FB9047dd39d9f418d5483ED600df';
const ARBITRUM_MAINNET_SFRXUSD_ADDRESS = '0x5Bff88cA1442c2496f7E475E9e7786383Bc070c0';
 
// Staking Parameters
const STAKE_AMOUNT = 10_000_000_000_000_000_000n; // 10 frxUSD (18 decimals)
const UNSTAKE_AMOUNT = 10_000_000_000_000_000_000n; // 10 sfrxUSD (18 decimals)
 
// Set up wallet client
const arbitrumWalletClient = createWalletClient({
  chain: arbitrum,
  transport: http(),
  account,
});
 
// Set up public client
const arbitrumPublicClient = createPublicClient({
  chain: arbitrum,
  transport: http(),
});
 
async function approveForStakingOrUnstaking(tokenAddress, amount) {
  console.log(`Approving frxUSD for staking on Arbitrum mainnet...`);
  const approveTx = await arbitrumWalletClient.sendTransaction({
    to: tokenAddress,
    data: encodeFunctionData({
      abi: [
        {
          type: 'function',
          name: 'approve',
          stateMutability: 'nonpayable',
          inputs: [
            { name: 'spender', type: 'address' },
            { name: 'amount', type: 'uint256' },
          ],
          outputs: [{ name: '', type: 'bool' }],
        },
      ],
      functionName: 'approve',
      args: [ARBITRUM_MAINNET_HOP_ADDRESS, amount],
    }),
  });
  console.log(`Arbitrum mainnet frxUSD Approval Tx: ${approveTx}`);
}
 
async function approveFrxusdForStaking() {
  await approveForStakingOrUnstaking(ARBITRUM_MAINNET_FRXUSD_ADDRESS, STAKE_AMOUNT);
}
 
async function approveSfrxusdForUnstaking() {
  await approveForStakingOrUnstaking(ARBITRUM_MAINNET_SFRXUSD_ADDRESS, UNSTAKE_AMOUNT);
}
 
async function retrieveQuote(tokenAddress, amount) {
  console.log('Retrieving quote...');
  const quote = await arbitrumPublicClient.readContract({
    address: ARBITRUM_MAINNET_HOP_ADDRESS,
    abi: [
      {
        inputs: [
          { internalType: 'address', name: '_oft', type: 'address' },
          { internalType: 'bytes32', name: '_to', type: 'bytes32' },
          { internalType: 'uint256', name: '_amountLD', type: 'uint256' },
        ],
        name: 'quote',
        outputs: [
          {
            components: [
              { internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
              { internalType: 'uint256', name: 'lzTokenFee', type: 'uint256' },
            ],
            internalType: 'struct MessagingFee',
            name: 'fee',
            type: 'tuple',
          },
        ],
        stateMutability: 'view',
        type: 'function',
      },
    ],
    functionName: 'quote',
    args: [
      tokenAddress,
      DESTINATION_ADDRESS_BYTES32, // Use bytes32 format
      amount,
    ],
  });
  const nativeFee = quote.nativeFee;
  return nativeFee;
}
 
async function stakeOrUnstakeFrxusd(tokenAddress, amount) {
  console.log(`Staking frxUSD for sfrxUSD on Arbitrum mainnet...`);
  const nativeFee = await retrieveQuote(tokenAddress, amount);
  const txHash = await arbitrumWalletClient.sendTransaction({
    to: ARBITRUM_MAINNET_HOP_ADDRESS,
    data: encodeFunctionData({
      abi: [
        {
          inputs: [
            { internalType: 'address', name: '_oft', type: 'address' },
            { internalType: 'uint256', name: '_amountLD', type: 'uint256' },
          ],
          name: 'mintRedeem',
          outputs: [],
          stateMutability: 'payable',
          type: 'function',
        },
      ],
      functionName: 'mintRedeem',
      args: [tokenAddress, amount],
    }),
    value: nativeFee,
  });
  console.log(`Arbitrum mainnet Stake Tx: ${txHash}`);
}
 
async function stakeFrxusd() {
  await stakeOrUnstakeFrxusd(ARBITRUM_MAINNET_FRXUSD_ADDRESS, STAKE_AMOUNT);
}
 
async function unstakeSfrxusd() {
  await stakeOrUnstakeFrxusd(ARBITRUM_MAINNET_SFRXUSD_ADDRESS, UNSTAKE_AMOUNT);
}
 
// ============ Main ============
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 transfer.js file.',
    );
    process.exit(1);
  }
  const command = process.argv[2];
 
  switch (command) {
    case 'stake-frxusd':
      console.log('Staking frxusd for sfrxusd...');
      await approveFrxusdForStaking();
      await stakeFrxusd();
      break;
    case 'unstake-sfrxusd':
      console.log('Unstaking sfrxusd for frxusd...');
      await approveSfrxusdForUnstaking();
      await unstakeSfrxusd();
      break;
    default:
      console.error('Invalid command. Please use "stake-frxusd" or "unstake-sfrxusd".');
      process.exit(1);
  }
 
  console.log('Transaction completed!');
}
 
main().catch(console.error);

Test the script

To test staking frxUSD for sfrxUSD, run the following command:

node stake-and-unstake.js stake-frxusd

To test unstaking sfrxUSD for frxUSD, run the following command:

node stake-and-unstake.js unstake-sfrxusd

Once each script runs and the staking and unstaking operations are finalized, the confirmation receipts are logged in the console.

What's next