diff --git a/docs/fassets/developer-guides.mdx b/docs/fassets/developer-guides.mdx index e652516a..1f19b31e 100644 --- a/docs/fassets/developer-guides.mdx +++ b/docs/fassets/developer-guides.mdx @@ -87,6 +87,12 @@ Guides for [redeeming FAssets](/fassets/redemption) back to underlying assets. href: "/fassets/developer-guides/fassets-redemption-queue", docId: "fassets/developer-guides/fassets-redemption-queue", }, + { + type: "link", + label: "FAsset Auto-Redemption", + href: "/fassets/developer-guides/fassets-autoredeem", + docId: "fassets/developer-guides/fassets-autoredeem", + }, ]} /> diff --git a/docs/fassets/developer-guides/12-fassets-autoredeem.mdx b/docs/fassets/developer-guides/12-fassets-autoredeem.mdx new file mode 100644 index 00000000..712d83e2 --- /dev/null +++ b/docs/fassets/developer-guides/12-fassets-autoredeem.mdx @@ -0,0 +1,585 @@ +--- +title: FAsset Auto-Redemption +tags: [intermediate, fassets] +slug: fassets-autoredeem +description: Auto-redeem FAssets to native XRP cross-chain +keywords: [fassets, flare-network] +sidebar_position: 12 +--- + +import CodeBlock from "@theme/CodeBlock"; +import FassetRedeemComposer from "!!raw-loader!/examples/developer-hub-solidity/FAssetRedeemComposer.sol"; +import bridgeToHyperEVM from "!!raw-loader!/examples/developer-hub-javascript/bridgeToHyperEVM.ts"; +import autoRedeemHyperEVM from "!!raw-loader!/examples/developer-hub-javascript/autoRedeemHyperEVM.ts"; +import getOftPeers from "!!raw-loader!/examples/developer-hub-javascript/getOftPeers.ts"; + +## Overview + +In this guide, you will learn how to bridge FAssets (specifically FXRP) between Flare Testnet Coston2 and Hyperliquid EVM Testnet using LayerZero's cross-chain messaging protocol, with support for **automatic redemption** - converting FXRP back to native XRP on the XRP Ledger in a single transaction. + +This guide covers two key functionalities: + +1. **Bridging FXRP from Flare Testnet Coston2 to Hyperliquid EVM** (`bridgeToHyperEVM.ts`) - Transfer wrapped XRP tokens to Hyperliquid for trading or DeFi use. +2. **Auto-Redeem from Hyperliquid to Underlying Asset** (`autoRedeemHyperEVM.ts`) - Send FXRP from Hyperliquid back to Flare Testnet Coston2 and automatically redeem it for native XRP. + +**Key technologies:** + +- [LayerZero OFT](https://docs.layerzero.network/v2/developers/evm/oft/quickstart) (Omnichain Fungible Token) for cross-chain token transfers. + OFT works by burning tokens on the source chain and minting equivalent tokens on the destination chain, enabling seamless movement of assets across different blockchains. +- Flare's [FAsset](/fassets/overview) system for tokenizing non-smart contract assets. +- [LayerZero Composer](https://docs.layerzero.network/v2/developers/evm/oft/composing) pattern for executing custom logic (like FAsset redemption) automatically when tokens arrive on the destination chain. + +This guide includes three main components: a Solidity smart contract (`FAssetRedeemComposer.sol`) and two TypeScript scripts (`bridgeToHyperEVM.ts` and `autoRedeemHyperEVM.ts`) that demonstrate the complete workflow. + +Clone the [Flare Hardhat Starter](https://github.com/flare-foundation/flare-hardhat-starter) to follow along. + +## Getting Started: The Two-Step Process + +:::warning + +To successfully test the auto-redemption feature, you must run the scripts in the correct order. +You must complete [Step 1](#step-1-bridge-fxrp-to-hyperliquid-evm-required-first) (bridging TO Hyperliquid) before you can execute [Step 2](#step-2-auto-redeem-to-native-xrp-requires-step-1) (auto-redeeming FROM Hyperliquid). + +::: + +### Step 1: Bridge FXRP to Hyperliquid EVM (Required First) + +Run [`bridgeToHyperEVM.ts`](https://github.com/flare-foundation/flare-hardhat-starter/blob/main/scripts/fassets/bridgeToHyperEVM.ts) on **Flare Testnet Coston2** network to transfer your FXRP tokens from Coston2 to Hyperliquid EVM Testnet. +This script does NOT involve auto-redemption - it simply moves your tokens to Hyperliquid where you can use them to prepare for the auto-redemption process. + +:::info + +You need FXRP tokens on Hyperliquid EVM before you can test the auto-redeem functionality. +The auto-redeem script will fail if you don't have tokens there. + +::: + +```bash +# Step 1: Bridge tokens TO Hyperliquid +yarn hardhat run scripts/fassets/bridgeToHyperEVM.ts --network coston2 +``` + +### Step 2: Auto-Redeem to Native XRP (Requires Step 1) + +Once you have FXRP tokens on Hyperliquid (from Step 1), run [`autoRedeemHyperEVM.ts`](https://github.com/flare-foundation/flare-hardhat-starter/blob/main/scripts/fassets/autoRedeemHyperEVM.ts) on **Hyperliquid Testnet** network to send them back to Flare Testnet Coston2 with automatic redemption to native XRP. +This is the auto-redemption feature that converts FXRP back to native XRP on the XRP Ledger in a single transaction. + +```bash +# Step 2: Auto-redeem back to native XRP +yarn hardhat run scripts/fassets/autoRedeemHyperEVM.ts --network hyperliquidTestnet +``` + +## FAssetRedeemComposer Contract + +### What It Is + +`FAssetRedeemComposer.sol` is a LayerZero Composer contract that automatically redeems FAssets to their underlying assets when tokens arrive on Flare Testnet Coston2 via LayerZero's compose message feature. + +### How It Works + +The composer implements the [IOAppComposer](https://docs.layerzero.network/v2/developers/evm/composer/overview) interface and processes LayerZero compose messages: + +1. **Receives Compose Message**: LayerZero endpoint calls [lzCompose()](https://docs.layerzero.network/v2/developers/evm/composer/overview#:~:text=/**%0A%20%20%20%20%20*%20%40notice%20Handles,messages%0A%20%20%20%20%7D%0A%7D) with the incoming OFT transfer and compose data. +2. **Extracts Parameters**: Decodes the compose message to get: + - Amount to redeem (in lots). + - Underlying XRP address on the XRP Ledger. + - Redeemer EVM address on Flare Chain. +3. **Gets Asset Manager**: Retrieves the FAsset AssetManager from Flare's ContractRegistry. +4. **Calculates Lots**: Determines how many lots can be redeemed based on received balance. +5. **Approves Tokens**: Grants AssetManager permission to spend the FAsset tokens (which is in the [ERC-20](https://eips.ethereum.org/EIPS/eip-20) standard). +6. **Executes Redemption**: Calls [`assetManager.redeem()`](/fassets/reference/IAssetManager#redeem) to burn FAssets and release underlying XRP. + + + {FassetRedeemComposer} + + +### Code Breakdown + +The `_processRedemption` function contains the core redemption logic with numbered steps in the comments: + +1. **Decode Message**: Extracts the underlying XRP address and redeemer EVM address from the compose message. +2. **Get Asset Manager & fXRP Token**: Retrieves the FAsset AssetManager. +3. **Check Balance**: Verifies that tokens were received from LayerZero. Reverts with `InsufficientBalance()` if balance is zero. +4. **Calculate Lots**: Determines how many complete lots can be redeemed based on the received balance and the lot size from AssetManager settings. +5. **Calculate Amount**: Computes the exact amount to redeem (must be a multiple of lot size). Reverts with `AmountTooSmall()` if less than 1 lot. +6. **Approve AssetManager**: Grants the AssetManager permission to spend the FAsset tokens using the ERC-20 `approve` pattern. +7. **Redeem**: Calls [`assetManager.redeem()`](/fassets/reference/IAssetManager#redeem) to burn FAssets and trigger the release of underlying XRP to the specified address. + +The contract also includes `recoverTokens()` and `recoverNative()` helper functions that allow the owner to recover any stuck tokens or native currency if needed. + +## Bridge FXRP to Hyperliquid EVM (Step 1) + +### What It Is + +**This is Step 1 of the two-step process** and is a **prerequisite** for testing the auto-redemption feature. + +This script bridges FXRP tokens from Flare Testnet Coston2 to Hyperliquid EVM Testnet using LayerZero's OFT Adapter pattern. +It wraps existing ERC20 FAsset tokens into LayerZero OFT format for cross-chain transfer. + +:::warning + +This script does NOT perform auto-redemption. + +::: +Its purpose is to get your FXRP tokens onto Hyperliquid EVM Testnet so that: + +- You can use them for trading or DeFi on Hyperliquid, OR +- You can run the auto-redeem script (Step 2) to convert them back to native XRP + +You must successfully complete this step before running the `autoRedeemHyperEVM.ts` script. + +### How It Works + +#### Step-by-Step Process + +1. **Balance Check**: Verifies user has sufficient FTestXRP tokens. +2. **Token Approval**: + - Approves OFT Adapter to spend FTestXRP (2x amount for safety). + - Approves Composer (if needed for future operations). +3. **Build Send Parameters**: + - Destination: Hyperliquid EVM Testnet (EndpointID for Hyperliquid Testnet: `40294`). + - Recipient: Same address on destination chain. + - Amount: Calculated from configured lots (default 1 lot, with 10% buffer). + - LayerZero options: Executor gas limit set to 200,000. +4. **Quote Fee**: Calculates the LayerZero cross-chain messaging fee. +5. **Execute Bridge**: Sends tokens via `oftAdapter.send()`. +6. **Confirmation**: Waits for transaction confirmation and provides tracking link to the LayerZero Explorer page. + +### Prerequisites + +- **Balance Requirements**: + - FTestXRP tokens (amount you want to bridge). + - C2FLR tokens (for gas fees + LayerZero fees). + You can get some from the Flare Testnet [faucet](https://faucet.flare.network/). +- **Environment Setup**: + - Private key configured in Hardhat for Flare Testnet Coston2 and Hyperliquid Testnet. + +### Configuration + +Edit the `CONFIG` object in the script: + +```typescript +const CONFIG = { + COSTON2_OFT_ADAPTER: "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639", + COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || "", + HYPERLIQUID_EID: EndpointId.HYPERLIQUID_V2_TESTNET, + EXECUTOR_GAS: 200_000, + BRIDGE_LOTS: "1", // Change this to your desired number of lots +}; +``` + +### How to Run + +**Run this script FIRST before attempting auto-redemption.** +This gets your FXRP tokens onto Hyperliquid EVM. + +1. **Install Dependencies**: + + ```bash + yarn install + ``` + +2. **Configure Environment**: + + ```bash + # .env file + COSTON2_RPC_URL=https://coston2-api.flare.network/ext/C/rpc + DEPLOYER_PRIVATE_KEY=your_private_key_here + COSTON2_COMPOSER=0x... # Optional for this step, required for Step 2 + ``` + +3. **Run the Script on Flare Testnet Coston2**: + + ```bash + yarn hardhat run scripts/fassets/bridgeToHyperEVM.ts --network coston2 + ``` + +4. **Wait for Completion**: + - Monitor the transaction on LayerZero Scan (link provided in output) + - Allow 2-5 minutes for cross-chain delivery + - Verify FXRP balance increased on Hyperliquid EVM before proceeding to Step 2 + +### Expected Output + +``` +Using account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + +šŸ“‹ Bridge Details: +From: Coston2 +To: Hyperliquid EVM Testnet +Amount: 11.0 FXRP +Recipient: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + +Your FTestXRP balance: 100.0 + +1ļøāƒ£ Checking OFT Adapter token address... + OFT Adapter's inner token: 0x8b4abA9C4BD7DD961659b02129beE20c6286e17F + Expected token: 0x8b4abA9C4BD7DD961659b02129beE20c6286e17F + Match: true + + Approving FTestXRP for OFT Adapter... + OFT Adapter address: 0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639 + Amount: 11.0 FXRP +āœ… OFT Adapter approved + Verified allowance: 22.0 FXRP + +3ļøāƒ£ LayerZero Fee: 0.001234 C2FLR + +4ļøāƒ£ Sending FXRP to Hyperliquid EVM Testnet... +Transaction sent: 0xabc123... +āœ… Confirmed in block: 12345678 + +šŸŽ‰ Success! Your FXRP is on the way to Hyperliquid EVM Testnet! + +Track your transaction: +https://testnet.layerzeroscan.com/tx/0xabc123... + +It may take a few minutes to arrive on Hyperliquid EVM Testnet. +``` + +
+View `bridgeToHyperEVM.ts` source code + + + {bridgeToHyperEVM} + + +
+ +### Troubleshooting + +**Error: Insufficient FTestXRP balance** + +- Solution: Acquire FTestXRP tokens on Flare Testnet Coston2 [faucet](https://faucet.flare.network/coston2) or follow our Fasset minting [guide](/fassets/developer-guides/fassets-mint). + +**Error: Insufficient C2FLR for gas** + +- Solution: Get C2FLR from Flare Testnet Coston2 [faucet](https://faucet.flare.network/coston2). + +**Error: Transaction reverted** + +- Check that the OFT Adapter address matches the FTestXRP token. +- Verify LayerZero endpoint is operational. +- Ensure gas limits are sufficient. + +## Auto-Redeem from Hyperliquid EVM (Step 2) + +### What It Is + +**This is Step 2 of the two-step process** and **requires you to have FXRP tokens on Hyperliquid EVM first** (from [Step 1](#step-1-bridge-fxrp-to-hyperliquid-evm-required-first)). + +This script sends FXRP from Hyperliquid EVM Testnet back to Flare Testnet Coston2 with **automatic redemption** to native XRP on the XRP Ledger. +It uses LayerZero's compose feature to trigger the `FAssetRedeemComposer` contract upon arrival. + +**Prerequisites:** + +- You must have FXRP OFT tokens on Hyperliquid EVM Testnet. +- If you don't have FXRP on Hyperliquid, run `bridgeToHyperEVM.ts` first ([Step 1](#step-1-bridge-fxrp-to-hyperliquid-evm-required-first)). +- The FAssetRedeemComposer contract must be deployed on Flare Testnet Coston2. + +### How It Works + +#### Flow Diagram + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Developer │ +│ (Hyperliquid EVM)│ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + │ 1. Has FXRP OFT + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ FXRP OFT (Hyperliquid) │ +│ Balance: 10 FXRP │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + │ 2. Send with Compose Message + │ - Destination: Coston2 Composer + │ - Compose Data: (amount, xrpAddress, redeemer) + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ LayerZero Endpoint │ +│ - lzSend() │ +│ - Compose enabled │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + │ 3. Cross-chain message + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Coston2 Endpoint │ +│ - lzReceive() │ +│ - lzCompose() │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + │ 4. Calls Composer + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ FAssetRedeemComposer │ +│ - Receives FXRP │ +│ - Calculates lots │ +│ - Calls AssetManager │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + │ 5. Redemption + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ FAsset AssetManager │ +│ - Burns FXRP │ +│ - Releases XRP │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + │ 6. XRP sent to address + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ XRP Ledger Address │ +│ rpHuw4b... │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +#### Step-by-Step Process + +1. **Validate Setup**: + - Checks that `COSTON2_COMPOSER` is configured. + - Gets the signer account. +2. **Prepare Redemption Parameters**: + - Number of lots to send (default: 1 lot, amount calculated from lot size). + - XRP address to receive native XRP. + - Redeemer address (EVM address). +3. **Connect to FXRP OFT**: Gets the OFT contract on Hyperliquid. +4. **Encode Compose Message**: + - Encodes `(uint256 amount, string xrpAddress, address redeemer)`. + - This tells the composer what to do when tokens arrive. +5. **Build LayerZero Options**: + - Executor gas for `lzReceive()`: 400,000 + - Compose gas for `lzCompose()`: 700,000 +6. **Build Send Parameters**: + - Destination: Coston2 (Endpoint ID: `40296`). + - Recipient: FAssetRedeemComposer contract. + - Amount: Calculated from configured lots (default 1 lot). + - Compose message included +7. **Check Balance**: Verifies user has sufficient FXRP OFT. +8. **Quote Fee**: Calculates LayerZero messaging fee. +9. **Execute Send**: Sends FXRP with compose message. +10. **Auto-Redemption**: On arrival, composer automatically redeems to XRP. + +### Prerequisites + +- **Network**: Must run on Hyperliquid EVM Testnet. +- **Balance Requirements**: + - FXRP OFT tokens on Hyperliquid (amount you want to redeem). + - HYPE tokens (for gas fees + LayerZero fees). +- **Deployed Contracts**: + - FAssetRedeemComposer must be deployed on Flare Testnet Coston2. + - Composer address must be set in `.env`. + +### Configuration + +Edit the `CONFIG` object in the script: + +```typescript +const CONFIG = { + HYPERLIQUID_FXRP_OFT: + process.env.HYPERLIQUID_FXRP_OFT || + "0x14bfb521e318fc3d5e92A8462C65079BC7d4284c", + COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || "", + COSTON2_EID: EndpointId.FLARE_V2_TESTNET, + EXECUTOR_GAS: 400_000, // Gas for receiving on Coston2 + COMPOSE_GAS: 700_000, // Gas for compose execution + SEND_LOTS: "1", // Number of lots to redeem + XRP_ADDRESS: "rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp", // Your XRP address +}; +``` + +### How to Run + +**PREREQUISITE: You must have FXRP tokens on Hyperliquid EVM Testnet before running this script.** + +If you don't have FXRP on Hyperliquid yet: + +- Run `bridgeToHyperEVM.ts` first ([Step 1](#step-1-bridge-fxrp-to-hyperliquid-evm-required-first)) +- Wait for the bridge transaction to complete (2-5 minutes) +- Verify your FXRP balance on Hyperliquid EVM before proceeding + +Once you have FXRP on Hyperliquid: + +1. **Deploy FAssetRedeemComposer** (first time only): + + ```bash + npx hardhat deploy --network coston2 --tags FAssetRedeemComposer + ``` + +2. **Configure Environment**: + + ```bash + # .env file + HYPERLIQUID_TESTNET_RPC_URL=https://api.hyperliquid-testnet.xyz/evm + DEPLOYER_PRIVATE_KEY=your_private_key_here + COSTON2_COMPOSER=0x... # Required! Set this after deploying the composer + HYPERLIQUID_FXRP_OFT=0x14bfb521e318fc3d5e92A8462C65079BC7d4284c + ``` + +3. **Update XRP Address**: + - Edit `CONFIG.XRP_ADDRESS` in the script to your XRP ledger address + - This is where you'll receive the native XRP + - Must be a valid XRP address format (starts with 'r') + +4. **Run the Script on Hyperliquid Testnet**: + ```bash + yarn hardhat run scripts/fassets/autoRedeemHyperEVM.ts --network hyperliquidTestnet + ``` + +### Expected Output + +``` +Using account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e +āœ“ Composer configured: 0x123... + +šŸ“‹ Redemption Parameters: +Amount: 10.0 FXRP +XRP Address: rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp +Redeemer: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + +Connecting to FXRP OFT on Hyperliquid EVM: 0x14bfb521e318fc3d5e92A8462C65079BC7d4284c + +āœ“ Connected to FXRP OFT: 0x14bfb521e318fc3d5e92A8462C65079BC7d4284c +OFT address: 0x14bfb521e318fc3d5e92A8462C65079BC7d4284c + +Compose message encoded + +šŸ’° Current FXRP balance: 25.0 +Sufficient balance + +šŸ’µ LayerZero Fee: 0.002456 HYPE + +šŸš€ Sending 10.0 FXRP to Coston2 with auto-redeem... +Target composer: 0x123... +Underlying address: rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp + +āœ“ Transaction sent: 0xdef456... +Waiting for confirmation... +āœ… Confirmed in block: 9876543 + +šŸŽ‰ Success! Your FXRP is on the way to Coston2! + +šŸ“Š Track your cross-chain transaction: +https://testnet.layerzeroscan.com/tx/0xdef456... + +ā³ The auto-redeem will execute once the message arrives on Coston2. +XRP will be sent to: rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp +``` + +
+View `autoRedeemHyperEVM.ts` source code + + + {autoRedeemHyperEVM} + + +
+ +## FAQ + +**Q: How long does bridging take?** +A: Typically 2-5 minutes for LayerZero message delivery + execution time. + +**Q: What's the minimum amount I can bridge?** +A: Any amount for bridging to Hyperliquid. For auto-redeem, minimum is 1 [lot](https://dev.flare.network/fassets/minting#lots) (10 FXRP for XRP). + +**Q: Can I bridge to a different address?** +A: Yes, edit the `recipientAddress` parameter in the scripts. + +**Q: What happens if compose execution fails?** +A: Tokens will be stuck in the composer. Owner can recover using `recoverTokens()`. + +**Q: Can I use this on mainnet?** +A: This is designed for testnet. For mainnet, update contract addresses, thoroughly test, and audit all code. + +**Q: How do I get FTestXRP on Flare Testnet Coston2?** +A: Use Flare's FAsset minting process via the AssetManager contract. + +**Q: What if I don't have HYPE tokens?** +A: Get them from Hyperliquid testnet [faucet](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/testnet-faucet) or DEX. + +## Discovering Available Bridge Routes + +The `getOftPeers.ts` utility script discovers all configured LayerZero peers for the FXRP OFT Adapter on Flare Testnet Coston2. +It scans all LayerZero V2 testnet endpoints to find which EVM chains have been configured as valid bridge destinations. + +Before bridging FXRP to another chain, you need to know which chains are supported. +This script: + +- Automatically discovers all configured peer addresses +- Shows which EVM chains you can bridge FXRP to/from +- Provides the peer contract addresses for each chain +- Outputs results in both human-readable and JSON formats + +#### How It Works + +1. **Loads V2 Testnet Endpoints**: Dynamically retrieves all LayerZero V2 testnet endpoint IDs from the [`@layerzerolabs/lz-definitions`](https://docs.layerzero.network/plugins#layerzerolabslz-definitions) package. +2. **Queries Peers**: For each endpoint, calls the `peers()` [function](https://coston2-explorer.flare.network/address/0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639?tab=read_write_proxy&source_address=0x82BC5e114D0843160be1cEaB8196Ba91f87473FE#0xbb0b6a53) on the OFT Adapter contract. +3. **Filters Results**: Only shows endpoints that have a non-zero peer address configured. +4. **Formats Output**: Displays results in a table format and as JSON for programmatic use. + +#### How to Run + +```bash +yarn hardhat run scripts/layerzero/getOFTPeers.ts --network coston2 +``` + +#### Expected Output + +``` +=== FXRP OFT Adapter Peers Discovery === + +OFT Adapter: 0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639 +Network: Coston2 (Flare Testnet) + +Scanning 221 LayerZero V2 Testnet endpoints... + +āœ… Bsc (EID: 40102): 0xac7c4a07670589cf83b134a843bfe86c45a4bf4e +āœ… Sepolia (EID: 40161): 0x81672c5d42f3573ad95a0bdfbe824faac547d4e6 +āœ… Hyperliquid (EID: 40362): 0x14bfb521e318fc3d5e92a8462c65079bc7d4284c + +============================================================ +SUMMARY: Configured Peers +============================================================ + +Found 3 configured peer(s): + +| Chain | EID | Peer Address | +|-------|-----|--------------| +| Bsc | 40102 | 0xac7c4a07670589cf83b134a843bfe86c45a4bf4e | +| Sepolia | 40161 | 0x81672c5d42f3573ad95a0bdfbe824faac547d4e6 | +| Hyperliquid | 40362 | 0x14bfb521e318fc3d5e92a8462c65079bc7d4284c | + +--- Available Routes --- +You can bridge FXRP to/from the following chains: + + • Bsc + • Sepolia + • Hyperliquid +``` + +Once you've identified available peers, you can: + +1. **Bridge to any discovered chain**: Update the destination EID in `bridgeToHyperEVM.ts` to target a different chain +2. **Verify peer addresses**: Use the peer addresses to interact with OFT contracts on other chains +3. **Build integrations**: Use the JSON output to programmatically determine available routes + +
+View `getOftPeers.ts` source code + + + {getOftPeers} + + +
+ +:::tip Next Steps +To continue your FAssets development journey, you can: + +- Learn how to [mint FXRP](/fassets/developer-guides/fassets-mint) +- Understand how to [redeem FXRP](/fassets/developer-guides/fassets-redeem) +- Explore [FAssets system settings](/fassets/operational-parameters) + ::: diff --git a/examples/developer-hub-javascript/autoRedeemHyperEVM.ts b/examples/developer-hub-javascript/autoRedeemHyperEVM.ts new file mode 100644 index 00000000..6d8f0997 --- /dev/null +++ b/examples/developer-hub-javascript/autoRedeemHyperEVM.ts @@ -0,0 +1,279 @@ +/** + * Example script to send FXRP from Hyperliquid EVM Testnet to Coston2 with automatic redemption + * + * This demonstrates how to: + * 1. Send OFT tokens from Hyperliquid EVM Testnet (where FXRP is an OFT) + * 2. Use LayerZero compose to trigger automatic redemption on Hyperliquid + * 3. Redeem the underlying asset (XRP) to a specified address + * + * Usage: + * yarn hardhat run scripts/fassets/autoRedeemHyperEVM.ts --network hyperliquidTestnet + */ + +import { ethers } from "hardhat"; +import { formatUnits, zeroPadValue, AbiCoder, Contract } from "ethers"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +import { EndpointId } from "@layerzerolabs/lz-definitions"; + +import { getAssetManagerFXRP } from "../utils/getters"; + +// Configuration - using existing deployed contracts +const CONFIG = { + HYPERLIQUID_FXRP_OFT: + process.env.HYPERLIQUID_FXRP_OFT || + "0x14bfb521e318fc3d5e92A8462C65079BC7d4284c", + COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || "", + COSTON2_EID: EndpointId.FLARE_V2_TESTNET, // Coston2 EID (destination) + EXECUTOR_GAS: 400_000, + COMPOSE_GAS: 700_000, + SEND_LOTS: "1", + XRP_ADDRESS: "rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp", // Change this to the XRP address you are auto-redeeming to +} as const; + +type RedemptionParams = { + amountToSend: bigint; + underlyingAddress: string; + redeemer: string; + signerAddress: string; + executor: string; +}; + +type SendParams = { + dstEid: EndpointId; + to: string; + amountLD: bigint; + minAmountLD: bigint; + extraOptions: string; + composeMsg: string; + oftCmd: string; +}; + +async function calculateAmountToSend(lots: bigint) { + const assetManager = await getAssetManagerFXRP(); + const lotSize = BigInt(await assetManager.lotSize()); + + return lotSize * lots; +} + +/** + * Gets the signer and validates composer is deployed + */ +async function validateSetup() { + const [signer] = await ethers.getSigners(); + + console.log("Using account:", signer.address); + + if (!CONFIG.COSTON2_COMPOSER) { + throw new Error( + "āŒ HYPERLIQUID_COMPOSER not set in .env!\n" + + " Deploy FAssetRedeemComposer first on Hyperliquid:\n" + + " npx hardhat deploy --network hyperliquid --tags FAssetRedeemComposer", + ); + } + + console.log("āœ“ Composer configured:", CONFIG.COSTON2_COMPOSER); + + return signer; +} + +/** + * Prepares redemption parameters + */ +async function prepareRedemptionParams( + signerAddress: string, +): Promise { + const amountToSend = await calculateAmountToSend(BigInt(CONFIG.SEND_LOTS)); + const underlyingAddress = CONFIG.XRP_ADDRESS; + const redeemer = signerAddress; + + console.log("\nšŸ“‹ Redemption Parameters:"); + console.log("Amount:", formatUnits(amountToSend.toString(), 6), "FXRP"); + console.log("XRP Address:", underlyingAddress); + console.log("Redeemer:", redeemer); + + const executor = "0x0000000000000000000000000000000000000000"; + + return { amountToSend, underlyingAddress, redeemer, signerAddress, executor }; +} + +/** + * Connects to the OFT contract on Hyperliquid EVM + */ +async function connectToOFT() { + console.log( + "Connecting to FXRP OFT on Hyperliquid EVM:", + CONFIG.HYPERLIQUID_FXRP_OFT, + ); + const oft = await ethers.getContractAt( + "FXRPOFT", + CONFIG.HYPERLIQUID_FXRP_OFT, + ); + + console.log("\nāœ“ Connected to FXRP OFT:", CONFIG.HYPERLIQUID_FXRP_OFT); + console.log("OFT address:", oft.target); + + return oft; +} + +/** + * Encodes the compose message with redemption details + * Format: (amountToRedeem, underlyingAddress, redeemer) + */ +function encodeComposeMessage(params: RedemptionParams): string { + const abiCoder = AbiCoder.defaultAbiCoder(); + // redeem(uint256 _lots, string memory _redeemerUnderlyingAddressString, executor address) + const composeMsg = abiCoder.encode( + ["uint256", "string", "address"], + [params.amountToSend, params.underlyingAddress, params.redeemer], + ); + + console.log("Compose message encoded"); + + return composeMsg; +} + +/** + * Builds LayerZero options with compose support + */ +function buildComposeOptions(): string { + const options = Options.newOptions() + .addExecutorLzReceiveOption(CONFIG.EXECUTOR_GAS, 0) + .addExecutorComposeOption(0, CONFIG.COMPOSE_GAS, 0); + + return options.toHex(); +} + +/** + * Builds the send parameters for LayerZero + */ +function buildSendParams( + params: RedemptionParams, + composeMsg: string, + options: string, +): SendParams { + return { + dstEid: CONFIG.COSTON2_EID, + to: zeroPadValue(CONFIG.COSTON2_COMPOSER, 32), + amountLD: params.amountToSend, + minAmountLD: params.amountToSend, + extraOptions: options, + composeMsg: composeMsg, + oftCmd: "0x", + }; +} + +/** + * Checks if user has sufficient FXRP balance + */ +async function checkBalance( + oft: Contract, + signerAddress: string, + amountToSend: bigint, +): Promise { + console.log("signer address", signerAddress); + console.log("oft.address", oft.address); + const balance = await oft.balanceOf(signerAddress); + console.log("signer address", signerAddress); + console.log("\nšŸ’° Current FXRP balance:", formatUnits(balance, 6)); + + if (balance < amountToSend) { + console.error("\nāŒ Insufficient FXRP balance!"); + console.log(" Required:", formatUnits(amountToSend, 6), "FXRP"); + console.log(" Available:", formatUnits(balance, 6), "FXRP"); + throw new Error("Insufficient FXRP balance"); + } + + console.log("Sufficient balance"); +} + +/** + * Quotes the LayerZero fee for the send transaction + */ +async function quoteFee( + oft: Contract, + sendParam: SendParams, +): Promise<{ nativeFee: bigint; lzTokenFee: bigint }> { + const result = await oft.quoteSend(sendParam, false); + const nativeFee = result.nativeFee; + const lzTokenFee = result.lzTokenFee; + + console.log("\nšŸ’µ LayerZero Fee:", formatUnits(nativeFee, 18), "HYPE"); + + return { nativeFee, lzTokenFee }; +} + +/** + * Executes the send with auto-redeem + */ +async function executeSendAndRedeem( + oft: Contract, + sendParam: SendParams, + nativeFee: bigint, + lzTokenFee: bigint, + params: RedemptionParams, +): Promise { + console.log( + "\nšŸš€ Sending", + formatUnits(params.amountToSend, 6), + "FXRP to Coston2 with auto-redeem...", + ); + console.log("Target composer:", CONFIG.COSTON2_COMPOSER); + console.log("Underlying address:", params.underlyingAddress); + + const tx = await oft.send( + sendParam, + { nativeFee, lzTokenFee }, + params.signerAddress, + { value: nativeFee }, + ); + + console.log("\nāœ“ Transaction sent:", tx.hash); + console.log("Waiting for confirmation..."); + + const receipt = await tx.wait(); + console.log("āœ… Confirmed in block:", receipt?.blockNumber); + + console.log("\nšŸŽ‰ Success! Your FXRP is on the way to Coston2!"); + console.log("\nšŸ“Š Track your cross-chain transaction:"); + console.log(`https://testnet.layerzeroscan.com/tx/${tx.hash}`); + console.log( + "\nā³ The auto-redeem will execute once the message arrives on Coston2.", + ); + console.log("XRP will be sent to:", params.underlyingAddress); +} + +async function main() { + // 1. Validate setup and get signer + const signer = await validateSetup(); + + // 2. Prepare redemption parameters + const params = await prepareRedemptionParams(signer.address); + + // 3. Connect to OFT contract + const oft = await connectToOFT(); + console.log("3. oft.address", oft.address); + + // 4. Encode compose message + const composeMsg = encodeComposeMessage(params); + + // 5. Build LayerZero options + const options = buildComposeOptions(); + + // 6. Build send parameters + const sendParam = buildSendParams(params, composeMsg, options); + + console.log("7. oft.address", oft.address); + // 7. Check balance + await checkBalance(oft, params.signerAddress, params.amountToSend); + + // 8. Quote fee + const { nativeFee, lzTokenFee } = await quoteFee(oft, sendParam); + + // 9. Execute send with auto-redeem + await executeSendAndRedeem(oft, sendParam, nativeFee, lzTokenFee, params); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/developer-hub-javascript/bridgeToHyperEVM.ts b/examples/developer-hub-javascript/bridgeToHyperEVM.ts new file mode 100644 index 00000000..d5f3664b --- /dev/null +++ b/examples/developer-hub-javascript/bridgeToHyperEVM.ts @@ -0,0 +1,273 @@ +/** + * Bridge FXRP from Coston2 to Hyperliquid EVM Testnet + * + * This script helps you get FXRP on Hyperliquid EVM Testnet by bridging from Coston2 + * + * Prerequisites: + * - FTestXRP tokens on Coston2 + * - CFLR on Coston2 for gas + * + * Usage: + * yarn hardhat run scripts/fassets/bridgeToHyperEVM.ts --network coston2 + */ + +import { web3 } from "hardhat"; +import { formatUnits } from "ethers"; +import { EndpointId } from "@layerzerolabs/lz-definitions"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +import { + IERC20Instance, + FAssetOFTAdapterInstance, +} from "../../typechain-types"; +import { getAssetManagerFXRP } from "../utils/getters"; + +// Get the contracts +const IERC20 = artifacts.require("IERC20"); +const FAssetOFTAdapter = artifacts.require("FAssetOFTAdapter"); + +const CONFIG = { + COSTON2_FTESTXRP: "0x8b4abA9C4BD7DD961659b02129beE20c6286e17F", + COSTON2_OFT_ADAPTER: "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639", + COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || "", + HYPERLIQUID_EID: EndpointId.HYPERLIQUID_V2_TESTNET, // Hyperliquid testnet EID + EXECUTOR_GAS: 200_000, + BRIDGE_LOTS: "1", +} as const; + +type BridgeParams = { + amountToBridge: bigint; + recipientAddress: string; + signerAddress: string; +}; + +type SendParams = { + dstEid: EndpointId; + to: string; + amountLD: string; + minAmountLD: string; + extraOptions: string; + composeMsg: string; + oftCmd: string; +}; + +async function calculateAmountToBridge(lots: bigint) { + const assetManager = await getAssetManagerFXRP(); + const lotSize = await assetManager.lotSize(); + const amountToBridge = lotSize * lots; + + return amountToBridge * BigInt(1.1); // 10% buffer +} + +/** + * Gets the signer and displays account information + */ +async function getSigner() { + const accounts = await web3.eth.getAccounts(); + const signerAddress = accounts[0]; + + console.log("Using account:", signerAddress); + console.log("Token address:", CONFIG.COSTON2_FTESTXRP); + + return signerAddress; +} + +/** + * Prepares bridge parameters + */ +async function prepareBridgeParams( + signerAddress: string, +): Promise { + const amountToBridge = await calculateAmountToBridge( + BigInt(CONFIG.BRIDGE_LOTS), + ); + const recipientAddress = signerAddress; + + console.log("\nšŸ“‹ Bridge Details:"); + console.log("From: Coston2"); + console.log("To: Hyperliquid EVM Testnet"); + console.log("Amount:", formatUnits(amountToBridge.toString(), 6), "FXRP"); + console.log("Recipient:", recipientAddress); + + return { amountToBridge, recipientAddress, signerAddress }; +} + +/** + * Checks if user has sufficient balance to bridge + */ +async function checkBalance(params: BridgeParams): Promise { + const fTestXRP: IERC20Instance = await IERC20.at(CONFIG.COSTON2_FTESTXRP); + + const balance = await fTestXRP.balanceOf(params.signerAddress); + console.log("\nYour FTestXRP balance:", formatUnits(balance.toString(), 6)); + + if (BigInt(balance.toString()) > params.amountToBridge) { + console.error("\nāŒ Insufficient FTestXRP balance!"); + console.log(" Token address: " + CONFIG.COSTON2_FTESTXRP); + throw new Error("Insufficient balance"); + } + + return fTestXRP; +} + +/** + * Approves OFT Adapter AND Composer to spend FTestXRP + */ +async function approveTokens( + fTestXRP: IERC20Instance, + amountToBridge: bigint, + signerAddress: string, +): Promise { + const oftAdapter: FAssetOFTAdapterInstance = await FAssetOFTAdapter.at( + CONFIG.COSTON2_OFT_ADAPTER, + ); + + console.log("\n1ļøāƒ£ Checking OFT Adapter token address..."); + const innerToken = await oftAdapter.token(); + console.log(" OFT Adapter's inner token:", innerToken); + console.log(" Expected token:", CONFIG.COSTON2_FTESTXRP); + console.log( + " Match:", + innerToken.toLowerCase() === CONFIG.COSTON2_FTESTXRP.toLowerCase(), + ); + + console.log("\n Approving FTestXRP for OFT Adapter..."); + console.log(" OFT Adapter address:", oftAdapter.address); + console.log(" Amount:", formatUnits(amountToBridge.toString(), 6), "FXRP"); + + // Approve a much larger amount to account for any potential fees + const largeAmount = amountToBridge * BigInt(2); + await fTestXRP.approve(oftAdapter.address, largeAmount.toString()); + console.log("āœ… OFT Adapter approved"); + + // Verify the allowance + const oftAdapterAllowance = await fTestXRP.allowance( + signerAddress, + oftAdapter.address, + ); + console.log( + " Verified allowance:", + formatUnits(oftAdapterAllowance.toString(), 6), + "FXRP", + ); + + console.log("\n2ļøāƒ£ Approving FTestXRP for Composer..."); + console.log(" Composer address:", CONFIG.COSTON2_COMPOSER); + await fTestXRP.approve(CONFIG.COSTON2_COMPOSER, amountToBridge.toString()); + console.log("āœ… Composer approved"); + + // Verify the allowance + const composerAllowance = await fTestXRP.allowance( + signerAddress, + CONFIG.COSTON2_COMPOSER, + ); + console.log( + " Verified allowance:", + formatUnits(composerAllowance.toString(), 6), + "FXRP", + ); + + return oftAdapter; +} + +/** + * Builds LayerZero send parameters + */ +function buildSendParams(params: BridgeParams): SendParams { + // See https://docs.layerzero.network/v2/tools/sdks/options#generating-options + const options = Options.newOptions().addExecutorLzReceiveOption( + CONFIG.EXECUTOR_GAS, + 0, + ); + // Review send parameters here: https://docs.layerzero.network/v2/developers/evm/oft/oft-patterns-extensions#:~:text=Sending%20Token%E2%80%8B,composeMsg%20in%20bytes. + return { + dstEid: CONFIG.HYPERLIQUID_EID as EndpointId, + to: web3.utils.padLeft(params.recipientAddress, 64), // 32 bytes = 64 hex chars + amountLD: params.amountToBridge.toString(), + minAmountLD: params.amountToBridge.toString(), + extraOptions: options.toHex(), + composeMsg: "0x", + oftCmd: "0x", + }; +} + +/** + * Quotes the LayerZero fee for the bridge transaction + */ +async function quoteFee( + oftAdapter: FAssetOFTAdapterInstance, + sendParam: SendParams, +): Promise { + const result = await oftAdapter.quoteSend(sendParam, false); + const nativeFee = web3.utils.toBN(result.nativeFee.toString()); + console.log( + "\n3ļøāƒ£ LayerZero Fee:", + formatUnits(nativeFee.toString(), 18), + "C2FLR", + ); + return nativeFee; +} + +/** + * Executes the bridge transaction + */ +async function executeBridge( + oftAdapter: FAssetOFTAdapterInstance, + sendParam: SendParams, + nativeFee: BN, + signerAddress: string, +): Promise { + console.log("\n4ļøāƒ£ Sending FXRP to Hyperliquid EVM Testnet..."); + + const tx = await oftAdapter.send( + sendParam, + { nativeFee: nativeFee.toString(), lzTokenFee: "0" }, + signerAddress, + { + value: nativeFee.toString(), + }, + ); + + console.log("Transaction sent:", tx.tx); + console.log("āœ… Confirmed in block:", tx.receipt.blockNumber); + + console.log( + "\nšŸŽ‰ Success! Your FXRP is on the way to Hyperliquid EVM Testnet!", + ); + console.log("\nTrack your transaction:"); + console.log(`https://testnet.layerzeroscan.com/tx/${tx.tx}`); + console.log( + "\nIt may take a few minutes to arrive on Hyperliquid EVM Testnet.", + ); +} + +async function main() { + // 1. Get signer and display account info + const signerAddress = await getSigner(); + + // 2. Prepare bridge parameters + const params = await prepareBridgeParams(signerAddress); + + // 3. Check balance and get token contract + const fTestXRP = await checkBalance(params); + + // 4. Approve tokens and get OFT adapter + const oftAdapter = await approveTokens( + fTestXRP, + params.amountToBridge, + signerAddress, + ); + + // 5. Build send parameters + const sendParam = buildSendParams(params); + + // 6. Quote the fee + const nativeFee = await quoteFee(oftAdapter, sendParam); + + // 7. Execute the bridge transaction + await executeBridge(oftAdapter, sendParam, nativeFee, signerAddress); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/developer-hub-javascript/getOftPeers.ts b/examples/developer-hub-javascript/getOftPeers.ts new file mode 100644 index 00000000..47d9c722 --- /dev/null +++ b/examples/developer-hub-javascript/getOftPeers.ts @@ -0,0 +1,129 @@ +/** + * Get all LayerZero peers for the FXRP OFT Adapter on Coston2 + * + * Usage: + * yarn hardhat run scripts/layerzero/getOFTPeers.ts --network coston2 + */ + +import { web3 } from "hardhat"; +import { EndpointId } from "@layerzerolabs/lz-definitions"; + +// FXRP OFT Adapter on Coston2 +const OFT_ADAPTER_ADDRESS = "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639"; + +// Minimal OApp ABI for peers function +const OAPP_ABI = [ + { + inputs: [{ internalType: "uint32", name: "eid", type: "uint32" }], + name: "peers", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, +]; + +// Get ALL V2 Testnet endpoints dynamically from the EndpointId enum +function getAllV2TestnetEndpoints(): { name: string; eid: number }[] { + const endpoints: { name: string; eid: number }[] = []; + + for (const [key, value] of Object.entries(EndpointId)) { + // Only include V2 testnet endpoints (they end with _V2_TESTNET and have numeric values) + if (key.endsWith("_V2_TESTNET") && typeof value === "number") { + // Convert key like "SEPOLIA_V2_TESTNET" to "Sepolia" + const name = key + .replace("_V2_TESTNET", "") + .split("_") + .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) + .join(" "); + endpoints.push({ name, eid: value }); + } + } + + // Sort by EID for consistent output + return endpoints.sort((a, b) => a.eid - b.eid); +} + +const V2_TESTNET_ENDPOINTS = getAllV2TestnetEndpoints(); + +const ZERO_BYTES32 = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + +async function main() { + console.log("=== FXRP OFT Adapter Peers Discovery ===\n"); + console.log(`OFT Adapter: ${OFT_ADAPTER_ADDRESS}`); + console.log(`Network: Coston2 (Flare Testnet)\n`); + + const oftAdapter = new web3.eth.Contract(OAPP_ABI, OFT_ADAPTER_ADDRESS); + + const configuredPeers: { name: string; eid: number; peer: string }[] = []; + const errors: { name: string; eid: number; error: string }[] = []; + + console.log( + `Scanning ${V2_TESTNET_ENDPOINTS.length} LayerZero V2 Testnet endpoints...\n`, + ); + + for (const endpoint of V2_TESTNET_ENDPOINTS) { + try { + const peer = await oftAdapter.methods.peers(endpoint.eid).call(); + + if (peer && peer !== ZERO_BYTES32) { + // Convert bytes32 to address (last 20 bytes) + const peerAddress = "0x" + peer.slice(-40); + configuredPeers.push({ + name: endpoint.name, + eid: endpoint.eid, + peer: peerAddress, + }); + console.log( + `āœ… ${endpoint.name} (EID: ${endpoint.eid}): ${peerAddress}`, + ); + } + } catch (error: unknown) { + // Some endpoints might not exist or the contract might revert + const errorMessage = + error instanceof Error ? error.message.slice(0, 50) : "Unknown error"; + errors.push({ + name: endpoint.name, + eid: endpoint.eid, + error: errorMessage, + }); + } + } + + console.log("\n" + "=".repeat(60)); + console.log("SUMMARY: Configured Peers"); + console.log("=".repeat(60) + "\n"); + + if (configuredPeers.length === 0) { + console.log("No peers configured for the FXRP OFT Adapter.\n"); + } else { + console.log(`Found ${configuredPeers.length} configured peer(s):\n`); + + console.log("| Chain | EID | Peer Address |"); + console.log("|-------|-----|--------------|"); + for (const peer of configuredPeers) { + console.log(`| ${peer.name} | ${peer.eid} | ${peer.peer} |`); + } + + console.log("\n--- Available Routes ---"); + console.log("You can bridge FXRP to/from the following chains:\n"); + for (const peer of configuredPeers) { + console.log(` • ${peer.name}`); + } + } + + if (errors.length > 0) { + console.log( + `\n(${errors.length} endpoints had errors or are not available)`, + ); + } + + // Export as JSON for programmatic use + console.log("\n--- JSON Output ---"); + console.log(JSON.stringify(configuredPeers, null, 2)); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/examples/developer-hub-solidity/FAssetRedeemComposer.sol b/examples/developer-hub-solidity/FAssetRedeemComposer.sol new file mode 100644 index 00000000..c7e7e4c9 --- /dev/null +++ b/examples/developer-hub-solidity/FAssetRedeemComposer.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {IOAppComposer} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; +import {OFTComposeMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {IAssetManager} from "@flarenetwork/flare-periphery-contracts/coston2/IAssetManager.sol"; +import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol"; + +contract FAssetRedeemComposer is IOAppComposer, Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + address public immutable endpoint; + + event RedemptionTriggered( + address indexed redeemer, + string underlyingAddress, + uint256 indexed amountRedeemed, + uint256 indexed lots + ); + + error OnlyEndpoint(); + error InsufficientBalance(); + error AmountTooSmall(); + + constructor(address _endpoint) Ownable(msg.sender) { + endpoint = _endpoint; + } + + receive() external payable {} + + function lzCompose( + address /* _from */, + bytes32 /* _guid */, + bytes calldata _message, + address /* _executor */, + bytes calldata /* _extraData */ + ) external payable override nonReentrant { + if (msg.sender != endpoint) revert OnlyEndpoint(); + + bytes memory composeMsg = OFTComposeMsgCodec.composeMsg(_message); + _processRedemption(composeMsg); + } + + function _processRedemption(bytes memory composeMsg) internal { + // 1. Decode message + (, string memory underlyingAddress, address redeemer) = abi.decode( + composeMsg, + (uint256, string, address) + ); + + // 2. Get Asset Manager & fXRP Token from Registry + IAssetManager assetManager = ContractRegistry.getAssetManagerFXRP(); + IERC20 fAssetToken = IERC20(address(assetManager.fAsset())); + + // 3. Check Actual Balance received from LayerZero + uint256 currentBalance = fAssetToken.balanceOf(address(this)); + if (currentBalance == 0) revert InsufficientBalance(); + + // 4. Calculate Lots + uint256 lotSizeUBA = assetManager.getSettings().lotSizeAMG; + uint256 lots = currentBalance / lotSizeUBA; + + if (lots == 0) revert AmountTooSmall(); + + // 5. Calculate amount to burn + uint256 amountToRedeem = lots * lotSizeUBA; + + // 6. Approve AssetManager to spend the tokens + fAssetToken.forceApprove(address(assetManager), amountToRedeem); + + // 7. Redeem + uint256 redeemedAmount = assetManager.redeem( + lots, + underlyingAddress, + payable(address(0)) + ); + + emit RedemptionTriggered( + redeemer, + underlyingAddress, + redeemedAmount, + lots + ); + } + + function recoverTokens( + address token, + address to, + uint256 amount + ) external onlyOwner { + IERC20(token).safeTransfer(to, amount); + } + + function recoverNative() external onlyOwner { + payable(owner()).transfer(address(this).balance); + } +}