From 82b35a424fe8a9575e22e2ebdf75149591df7971 Mon Sep 17 00:00:00 2001 From: 0xreflexivity Date: Wed, 25 Feb 2026 13:27:31 -0500 Subject: [PATCH] feat(fassets): update auto-redeem guides for shared FAssetRedeemComposer Update auto-redemption docs and examples to use the shared pre-deployed FAssetRedeemComposer contract (0x80c5eb...) on Coston2. - Update compose encoding to match RedeemComposeData struct (address, string) - Add COMPOSE_VALUE for executor fee forwarding - Replace per-user composer deployment with shared contract references - Update FAssetRedeemComposer.sol to match deployed implementation - Convert ASCII flow diagrams to Mermaid sequence diagrams --- docs/fxrp/oft/fassets-autoredeem.mdx | 377 ++++++-------- .../autoRedeemFromHyperCore.ts | 15 +- .../autoRedeemFromHyperEVM.ts | 18 +- .../FAssetRedeemComposer.sol | 470 +++++++++++++++--- 4 files changed, 571 insertions(+), 309 deletions(-) diff --git a/docs/fxrp/oft/fassets-autoredeem.mdx b/docs/fxrp/oft/fassets-autoredeem.mdx index cec4c9c8..ecd68395 100644 --- a/docs/fxrp/oft/fassets-autoredeem.mdx +++ b/docs/fxrp/oft/fassets-autoredeem.mdx @@ -33,7 +33,7 @@ This guide covers four key functionalities: - [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. - [Hyperliquid spotSend API](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#spot-transfer) for transferring tokens between HyperCore and HyperEVM. -This guide includes a Solidity smart contract (`FAssetRedeemComposer.sol`) and four TypeScript scripts (`bridgeToHyperEVM.ts`, `bridgeToHyperCore.ts`, `autoRedeemFromHyperEVM.ts`, and `autoRedeemFromHyperCore.ts`) that demonstrate the complete workflows. +A shared [`FAssetRedeemComposer`](https://coston2-explorer.flare.network/address/0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52?tab=contract) contract is pre-deployed on Coston2, so users do not need to deploy their own. This guide includes four TypeScript scripts (`bridgeToHyperEVM.ts`, `bridgeToHyperCore.ts`, `autoRedeemFromHyperEVM.ts`, and `autoRedeemFromHyperCore.ts`) that demonstrate the complete workflows. Clone the [Flare Hardhat Starter](https://github.com/flare-foundation/flare-hardhat-starter) to follow along. @@ -77,39 +77,65 @@ yarn hardhat run scripts/fassets/autoRedeemFromHyperEVM.ts --network hyperliquid ### 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. +The [`FAssetRedeemComposer`](https://coston2-explorer.flare.network/address/0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52?tab=contract) is a shared LayerZero Composer contract **pre-deployed on Coston2** that automatically redeems FAssets to their underlying assets when tokens arrive 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: +The composer implements the [ILayerZeroComposer](https://docs.layerzero.network/v2/developers/evm/composer/overview) interface and creates per-user `FAssetRedeemerAccount` contracts automatically: 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} - +2. **Decodes `RedeemComposeData`**: Extracts the redeemer's EVM address and their underlying XRP address from the compose message. +3. **Creates Redeemer Account**: Deploys (or reuses) a deterministic per-user `FAssetRedeemerAccount` contract via CREATE2. +4. **Deducts Composer Fee**: Takes a percentage fee (configurable per source chain, currently 1%) from the received FXRP. +5. **Transfers Tokens**: Sends the remaining FXRP to the redeemer account. +6. **Executes Redemption**: The redeemer account calls [`assetManager.redeem()`](/fassets/reference/IAssetManager#redeem) to burn FAssets and release underlying XRP. + +### Compose Message Format + +The auto-redeem scripts encode the compose message using the `RedeemComposeData` struct: + +```solidity +struct RedeemComposeData { + /// @notice EVM address that owns the per-redeemer account. + address redeemer; + /// @notice Underlying-chain redemption destination passed to the asset manager. + string redeemerUnderlyingAddress; +} +``` + +In TypeScript, this is encoded as: + +```typescript +const composeMsg = web3.eth.abi.encodeParameters( + ["address", "string"], + [redeemer, xrpAddress], +); +``` + +### Executor Fee -### Code Breakdown +The `lzCompose` call must include native value to cover the executor fee on Coston2. This is set via `addExecutorLzComposeOption`: -The `_processRedemption` function contains the core redemption logic with numbered steps in the comments: +```typescript +const options = Options.newOptions() + .addExecutorLzReceiveOption(EXECUTOR_GAS, 0) + .addExecutorComposeOption(0, COMPOSE_GAS, COMPOSE_VALUE); +``` -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 current executor fee can be queried via `getExecutorData()` on the [composer contract](https://coston2-explorer.flare.network/address/0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52?tab=contract). -The contract also includes `recoverTokens()` and `recoverNative()` helper functions that allow the owner to recover any stuck tokens or native currency if needed. +### Contract Source + +
+View `FAssetRedeemComposer.sol` + + + {FassetRedeemComposer} + + +The full verified source is available on the [Coston2 explorer](https://coston2-explorer.flare.network/address/0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52?tab=contract). + +
## Bridge FXRP to Hyperliquid EVM (Step 1) @@ -190,7 +216,7 @@ This gets your FXRP tokens onto Hyperliquid EVM. # .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 + COSTON2_COMPOSER=0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52 ``` 3. **Run the Script on Flare Testnet Coston2**: @@ -287,51 +313,21 @@ Choose based on whether you want to use FXRP in smart contracts (HyperEVM) or tr #### Flow Diagram -``` -┌─────────────────────────┐ -│ Developer │ -│ (Coston2) │ -└────────┬────────────────┘ - │ - │ 1. Has FXRP tokens - ▼ -┌─────────────────────────┐ -│ FXRP (Coston2) │ -│ Balance: 11 FXRP │ -└────────┬────────────────┘ - │ - │ 2. Send via OFT Adapter - │ with Compose Message - ▼ -┌─────────────────────────┐ -│ LayerZero Endpoint │ -│ - lzSend() │ -│ - Compose enabled │ -└────────┬────────────────┘ - │ - │ 3. Cross-chain message - ▼ -┌─────────────────────────┐ -│ HyperEVM Endpoint │ -│ - lzReceive() │ -│ - lzCompose() │ -└────────┬────────────────┘ - │ - │ 4. Calls Composer - ▼ -┌─────────────────────────┐ -│ HyperliquidComposer │ -│ - Receives FXRP │ -│ - Transfers to system │ -│ address │ -└────────┬────────────────┘ - │ - │ 5. HyperCore credit - ▼ -┌─────────────────────────┐ -│ HyperCore Spot Wallet │ -│ FXRP credited │ -└─────────────────────────┘ +```mermaid +sequenceDiagram + participant Dev as Developer (Coston2) + participant OFT as OFT Adapter (Coston2) + participant LZ as LayerZero + participant EP as HyperEVM Endpoint + participant Comp as HyperliquidComposer
(shared, pre-deployed) + participant HC as HyperCore Spot Wallet + + Dev->>OFT: 1. Send FXRP via OFT Adapter
with Compose Message + OFT->>LZ: 2. lzSend() + LZ->>EP: 3. Cross-chain message + EP->>Comp: 4. lzReceive() + lzCompose() + Comp->>HC: 5. Transfer to system address + Note over HC: FXRP credited to
spot wallet ``` #### Step-by-Step Process @@ -360,7 +356,7 @@ Choose based on whether you want to use FXRP in smart contracts (HyperEVM) or tr You can get some from the Flare Testnet [faucet](https://faucet.flare.network/). - **Deployed Contracts**: - HyperliquidComposer must be deployed on HyperEVM Testnet. - - Composer address must be set in `.env`. + - Set `COSTON2_COMPOSER=0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52` in `.env`. ### Configuration @@ -492,66 +488,30 @@ It uses LayerZero's compose feature to trigger the `FAssetRedeemComposer` contra - 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. +- The shared FAssetRedeemComposer is pre-deployed on Coston2 at `0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52`. ### 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... │ -└─────────────────────────┘ +```mermaid +sequenceDiagram + participant Dev as Developer (HyperEVM) + participant OFT as FXRP OFT (HyperEVM) + participant LZ as LayerZero + participant EP as Coston2 Endpoint + participant Comp as FAssetRedeemComposer
(shared, pre-deployed) + participant Acct as FAssetRedeemerAccount
(per-user, CREATE2) + participant XRP as XRP Ledger Address + + Dev->>OFT: 1. send() with Compose Message
Data: (redeemer, xrpAddress) + OFT->>LZ: 2. lzSend() + LZ->>EP: 3. Cross-chain message + EP->>Comp: 4. lzReceive() + lzCompose() + Comp->>Comp: 5. Deduct composer fee + Comp->>Acct: 6. Transfer FXRP to redeemer account + Acct->>Acct: 7. assetManager.redeem()
Burns FXRP + Acct-->>XRP: 8. XRP sent to underlying address ``` #### Step-by-Step Process @@ -561,11 +521,11 @@ It uses LayerZero's compose feature to trigger the `FAssetRedeemComposer` contra - Gets the signer account. 2. **Connect to FXRP OFT**: Gets the OFT contract on Hyperliquid using the `FXRPOFT` artifact. 3. **Prepare Redemption Parameters**: - - Number of lots to send (default: 1 lot, amount calculated using `calculateAmountToSend` utility). + - Number of lots to send (default: 1 lot). - XRP address to receive native XRP. - Redeemer address (EVM address). 4. **Encode Compose Message**: - - Encodes `(uint256 amount, string xrpAddress, address redeemer)`. + - Encodes `(address redeemer, string xrpAddress)` matching the `RedeemComposeData` struct. - This tells the composer what to do when tokens arrive. 5. **Build LayerZero Options**: - Executor gas for `lzReceive()`: 1,000,000 @@ -587,8 +547,8 @@ It uses LayerZero's compose feature to trigger the `FAssetRedeemComposer` contra - 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`. + - The shared FAssetRedeemComposer is pre-deployed on Coston2 at `0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52`. + - Set `COSTON2_COMPOSER=0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52` in `.env`. ### Configuration @@ -599,12 +559,15 @@ const CONFIG = { HYPERLIQUID_FXRP_OFT: process.env.HYPERLIQUID_FXRP_OFT || "0x14bfb521e318fc3d5e92A8462C65079BC7d4284c", + // Shared FAssetRedeemComposer (pre-deployed on Coston2) COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || - "0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E", + "0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52", COSTON2_EID: EndpointId.FLARE_V2_TESTNET, EXECUTOR_GAS: 1_000_000, // Gas for receiving on Coston2 COMPOSE_GAS: 1_000_000, // Gas for compose execution + // Native value forwarded to cover executor fee on Coston2 + COMPOSE_VALUE: BigInt("1000000000000000"), // 0.001 ETH SEND_LOTS: "1", // Number of lots to redeem XRP_ADDRESS: "rpHuw4bKSjonKRrKKVYYVedg1jyPrmp", // Your XRP address }; @@ -622,21 +585,17 @@ If you don't have FXRP on Hyperliquid yet: 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 - ``` +```bash +# .env file +HYPERLIQUID_TESTNET_RPC_URL=https://api.hyperliquid-testnet.xyz/evm +DEPLOYER_PRIVATE_KEY=your_private_key_here +COSTON2_COMPOSER=0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52 +HYPERLIQUID_FXRP_OFT=0x14bfb521e318fc3d5e92A8462C65079BC7d4284c +```` 3. **Update XRP Address**: - Edit `CONFIG.XRP_ADDRESS` in the script to your XRP ledger address @@ -719,82 +678,33 @@ Tokens must be transferred from HyperCore to HyperEVM before they can be bridged - You must have FXRP tokens in your HyperCore spot wallet. - You need HYPE tokens on HyperEVM for LayerZero fees. -- The FAssetRedeemComposer contract must be deployed on Flare Testnet Coston2. +- The shared FAssetRedeemComposer is pre-deployed on Coston2 at `0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52`. ### How It Works #### Flow Diagram -``` -┌─────────────────────────┐ -│ Developer │ -│ (HyperCore Spot) │ -└────────┬────────────────┘ - │ - │ 1. Has FXRP in spot wallet - ▼ -┌─────────────────────────┐ -│ HyperCore Spot Wallet │ -│ Balance: 10 FXRP │ -└────────┬────────────────┘ - │ - │ 2. spotSend to system address - │ (EIP-712 signed) - ▼ -┌─────────────────────────┐ -│ Hyperliquid API │ -│ - /exchange endpoint │ -│ - spotSend action │ -└────────┬────────────────┘ - │ - │ 3. Tokens appear on HyperEVM - ▼ -┌─────────────────────────┐ -│ FXRP OFT (HyperEVM) │ -│ Balance: 10 FXRP │ -└────────┬────────────────┘ - │ - │ 4. Send with Compose Message - │ - Destination: Coston2 Composer - │ - Compose Data: (amount, xrpAddress, redeemer) - ▼ -┌─────────────────────────┐ -│ LayerZero Endpoint │ -│ - lzSend() │ -│ - Compose enabled │ -└────────┬────────────────┘ - │ - │ 5. Cross-chain message - ▼ -┌─────────────────────────┐ -│ Coston2 Endpoint │ -│ - lzReceive() │ -│ - lzCompose() │ -└────────┬────────────────┘ - │ - │ 6. Calls Composer - ▼ -┌─────────────────────────┐ -│ FAssetRedeemComposer │ -│ - Receives FXRP │ -│ - Calculates lots │ -│ - Calls AssetManager │ -└────────┬────────────────┘ - │ - │ 7. Redemption - ▼ -┌─────────────────────────┐ -│ FAsset AssetManager │ -│ - Burns FXRP │ -│ - Releases XRP │ -└────────┬────────────────┘ - │ - │ 8. XRP sent to address - ▼ -┌─────────────────────────┐ -│ XRP Ledger Address │ -│ rpHuw4b... │ -└─────────────────────────┘ +```mermaid +sequenceDiagram + participant Dev as Developer (HyperCore) + participant API as Hyperliquid API + participant OFT as FXRP OFT (HyperEVM) + participant LZ as LayerZero + participant EP as Coston2 Endpoint + participant Comp as FAssetRedeemComposer
(shared, pre-deployed) + participant Acct as FAssetRedeemerAccount
(per-user, CREATE2) + participant XRP as XRP Ledger Address + + Dev->>API: 1. spotSend to system address
(EIP-712 signed) + API->>OFT: 2. Tokens appear on HyperEVM + Dev->>OFT: 3. send() with Compose Message
Data: (redeemer, xrpAddress) + OFT->>LZ: 4. lzSend() + LZ->>EP: 5. Cross-chain message + EP->>Comp: 6. lzReceive() + lzCompose() + Comp->>Comp: 7. Deduct composer fee + Comp->>Acct: 8. Transfer FXRP to redeemer account + Acct->>Acct: 9. assetManager.redeem()
Burns FXRP + Acct-->>XRP: 10. XRP sent to underlying address ``` #### Step-by-Step Process @@ -816,7 +726,7 @@ Tokens must be transferred from HyperCore to HyperEVM before they can be bridged - Redeemer address (EVM address). 6. **Check HyperEVM Balance**: Verifies tokens arrived from HyperCore. 7. **Encode Compose Message**: - - Encodes `(uint256 amount, string xrpAddress, address redeemer)`. + - Encodes `(address redeemer, string xrpAddress)` matching the `RedeemComposeData` struct. - This tells the composer what to do when tokens arrive. 8. **Build LayerZero Options**: - Executor gas for `lzReceive()`: 1,000,000 @@ -865,8 +775,8 @@ FXRP_SYSTEM_ADDRESS: "0x20000000000000000000000000000000000005a3"; - FXRP tokens in your HyperCore spot wallet (amount you want to redeem). - HYPE tokens on HyperEVM (for gas fees + LayerZero fees). - **Deployed Contracts**: - - FAssetRedeemComposer must be deployed on Flare Testnet Coston2. - - Composer address must be set in `.env`. + - The shared FAssetRedeemComposer is pre-deployed on Coston2 at `0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52`. + - Set `COSTON2_COMPOSER=0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52` in `.env`. ### Configuration @@ -883,12 +793,15 @@ const CONFIG = { // System address for FXRP on testnet (token index 1443 = 0x5A3) FXRP_SYSTEM_ADDRESS: "0x20000000000000000000000000000000000005a3", FXRP_TOKEN_ID: "FXRP:0x2af78df5b575b45eea8a6a1175026dd6", + // Shared FAssetRedeemComposer (pre-deployed on Coston2) COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || - "0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E", + "0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52", COSTON2_EID: EndpointId.FLARE_V2_TESTNET, EXECUTOR_GAS: 1_000_000, // Gas for receiving on Coston2 COMPOSE_GAS: 1_000_000, // Gas for compose execution + // Native value forwarded to cover executor fee on Coston2 + COMPOSE_VALUE: BigInt("1000000000000000"), // 0.001 HYPE SEND_LOTS: "1", // Number of lots to redeem XRP_ADDRESS: process.env.XRP_ADDRESS || "rpHuw4bKSjonKRrKKVYYVedg1jyPrmp", HYPERLIQUID_CHAIN: "Testnet", @@ -899,22 +812,18 @@ const CONFIG = { **PREREQUISITE: You must have FXRP tokens in your HyperCore spot wallet before running this script.** -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 - XRP_ADDRESS=rYourXRPAddressHere # Your XRP ledger address - ``` +```bash +# .env file +HYPERLIQUID_TESTNET_RPC_URL=https://api.hyperliquid-testnet.xyz/evm +DEPLOYER_PRIVATE_KEY=your_private_key_here +COSTON2_COMPOSER=0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52 +HYPERLIQUID_FXRP_OFT=0x14bfb521e318fc3d5e92A8462C65079BC7d4284c +XRP_ADDRESS=rYourXRPAddressHere # Your XRP ledger address +```` 3. **Ensure you have FXRP on HyperCore**: - Bridge FXRP to HyperCore using `bridgeToHyperEVM.ts` followed by a transfer to HyperCore. @@ -934,7 +843,7 @@ FXRP Auto-Redemption from HyperCore HyperCore → HyperEVM → Coston2 → XRP Ledger ============================================================ Using account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e -✓ Coston2 Composer configured: 0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E +✓ Coston2 Composer configured: 0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52 📊 Checking HyperCore spot balance... HyperCore FXRP balance: 25.0 @@ -964,7 +873,7 @@ Compose message encoded for auto-redemption 📤 Step 2: Sending FXRP from HyperEVM to Coston2 with auto-redeem... Amount: 10.0 FXRP - Target composer: 0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E + Target composer: 0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52 XRP destination: rpHuw4bKSjonKRrKKVYYVedg1jyPrmp ✓ Transaction sent: 0xabc123... diff --git a/examples/developer-hub-javascript/autoRedeemFromHyperCore.ts b/examples/developer-hub-javascript/autoRedeemFromHyperCore.ts index de92b817..941036ae 100644 --- a/examples/developer-hub-javascript/autoRedeemFromHyperCore.ts +++ b/examples/developer-hub-javascript/autoRedeemFromHyperCore.ts @@ -37,10 +37,12 @@ const CONFIG = { FXRP_TOKEN_ID: "FXRP:0x2af78df5b575b45eea8a6a1175026dd6", COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || - "0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E", + "0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52", COSTON2_EID: EndpointId.FLARE_V2_TESTNET, EXECUTOR_GAS: 1_000_000, COMPOSE_GAS: 1_000_000, + // Native value forwarded to FAssetRedeemerAccount to cover executor fee on Coston2 + COMPOSE_VALUE: BigInt("1000000000000000"), // 0.001 HYPE SEND_LOTS: "1", XRP_ADDRESS: process.env.XRP_ADDRESS || "rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp", HYPERLIQUID_CHAIN: "Testnet", @@ -251,9 +253,10 @@ async function connectToOFT(): Promise { * Encodes the compose message for auto-redemption */ function encodeComposeMessage(params: RedemptionParams): string { + // Matches the RedeemComposeData struct on the shared FAssetRedeemComposer const composeMsg = web3.eth.abi.encodeParameters( - ["uint256", "string", "address"], - [params.amountToSend.toString(), params.underlyingAddress, params.redeemer], + ["address", "string"], + [params.redeemer, params.underlyingAddress], ); console.log("Compose message encoded for auto-redemption"); @@ -267,7 +270,11 @@ function encodeComposeMessage(params: RedemptionParams): string { function buildComposeOptions(): string { const options = Options.newOptions() .addExecutorLzReceiveOption(CONFIG.EXECUTOR_GAS, 0) - .addExecutorComposeOption(0, CONFIG.COMPOSE_GAS, 0); + .addExecutorComposeOption( + 0, + CONFIG.COMPOSE_GAS, + CONFIG.COMPOSE_VALUE.toString(), + ); return options.toHex(); } diff --git a/examples/developer-hub-javascript/autoRedeemFromHyperEVM.ts b/examples/developer-hub-javascript/autoRedeemFromHyperEVM.ts index 062d30d0..5e7d73fe 100644 --- a/examples/developer-hub-javascript/autoRedeemFromHyperEVM.ts +++ b/examples/developer-hub-javascript/autoRedeemFromHyperEVM.ts @@ -26,10 +26,12 @@ const CONFIG = { "0x14bfb521e318fc3d5e92A8462C65079BC7d4284c", COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || - "0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E", + "0x80c5ebBD65b4857CA2f917EAB1e9F83bcC947c52", COSTON2_EID: EndpointId.FLARE_V2_TESTNET, EXECUTOR_GAS: 1_000_000, COMPOSE_GAS: 1_000_000, + // Native value forwarded to FAssetRedeemerAccount to cover executor fee on Coston2 + COMPOSE_VALUE: BigInt("1000000000000000"), // 0.001 ETH SEND_LOTS: "1", XRP_ADDRESS: "rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp", } as const; @@ -117,13 +119,13 @@ async function connectToOFT(): Promise { /** * Encodes the compose message with redemption details - * Format: (amountToRedeem, underlyingAddress, redeemer) + * Format: (address redeemer, string redeemerUnderlyingAddress) + * Matches the RedeemComposeData struct on the shared FAssetRedeemComposer */ function encodeComposeMessage(params: RedemptionParams): string { - // redeem(uint256 _lots, string memory _redeemerUnderlyingAddressString, executor address) const composeMsg = web3.eth.abi.encodeParameters( - ["uint256", "string", "address"], - [params.amountToSend.toString(), params.underlyingAddress, params.redeemer], + ["address", "string"], + [params.redeemer, params.underlyingAddress], ); console.log("Compose message encoded"); @@ -137,7 +139,11 @@ function encodeComposeMessage(params: RedemptionParams): string { function buildComposeOptions(): string { const options = Options.newOptions() .addExecutorLzReceiveOption(CONFIG.EXECUTOR_GAS, 0) - .addExecutorComposeOption(0, CONFIG.COMPOSE_GAS, 0); + .addExecutorComposeOption( + 0, + CONFIG.COMPOSE_GAS, + CONFIG.COMPOSE_VALUE.toString(), + ); return options.toHex(); } diff --git a/examples/developer-hub-solidity/FAssetRedeemComposer.sol b/examples/developer-hub-solidity/FAssetRedeemComposer.sol index c7e7e4c9..98168546 100644 --- a/examples/developer-hub-solidity/FAssetRedeemComposer.sol +++ b/examples/developer-hub-solidity/FAssetRedeemComposer.sol @@ -1,101 +1,441 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.25; +pragma solidity ^0.8.27; -import {IOAppComposer} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; -import {OFTComposeMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; +import {IAssetManager} from "flare-periphery/src/flare/IAssetManager.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"; +import {ILayerZeroComposer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol"; +import {OFTComposeMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {IIFAssetRedeemerAccount} from "../interface/IIFAssetRedeemerAccount.sol"; +import {FAssetRedeemerAccountProxy} from "../proxy/FAssetRedeemerAccountProxy.sol"; +import {IFAssetRedeemComposer} from "../../userInterfaces/IFAssetRedeemComposer.sol"; +import {IBeacon} from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; +import {OwnableWithTimelock} from "../../utils/implementation/OwnableWithTimelock.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -contract FAssetRedeemComposer is IOAppComposer, Ownable, ReentrancyGuard { +/** + * @title FAssetRedeemComposer + * @notice LayerZero compose handler that orchestrates deterministic redeemer accounts and f-asset redemption. + */ +contract FAssetRedeemComposer is + IFAssetRedeemComposer, + OwnableWithTimelock, + UUPSUpgradeable, + ReentrancyGuardTransient +{ using SafeERC20 for IERC20; - address public immutable endpoint; + uint256 private constant PPM_DENOMINATOR = 1_000_000; + + /// @notice Mapping of redeemer to deterministic redeemer account address. + mapping(address redeemer => address redeemerAccount) + private redeemerToRedeemerAccount; - event RedemptionTriggered( - address indexed redeemer, - string underlyingAddress, - uint256 indexed amountRedeemed, - uint256 indexed lots - ); + /// @notice Trusted endpoint allowed to invoke `lzCompose`. + address public endpoint; + /// @notice Asset manager used for f-asset redemption. + IAssetManager public assetManager; + /// @notice FAsset token. + IERC20 public fAsset; + /// @notice Stable coin token - returned in case of a redemption failure. + IERC20 public stableCoin; + /// @notice Wrapped native token - returned in case of a redemption failure if stable coin balance is insufficient. + IERC20 public wNat; + /// @notice Trusted source OApp address (FAssetOFTAdapter). + address public trustedSourceOApp; + /// @notice Current beacon implementation for redeemer account proxies. + address public redeemerAccountImplementation; + /// @notice Recipient of composer fee collected in fAsset. + address public composerFeeRecipient; + /// @notice Default composer fee in PPM. + uint256 public defaultComposerFeePPM; + /// @notice Optional srcEid-specific composer fee in PPM, stored as fee + 1 to distinguish unset values. + mapping(uint32 srcEid => uint256 feePPM) private composerFeesPPM; + /// @notice The redeem executor. + address payable private executor; + /// @notice The native fee expected by the executor for redeem execution. + uint256 private executorFee; - error OnlyEndpoint(); - error InsufficientBalance(); - error AmountTooSmall(); + /** + * @notice Disables initializers on implementation contract. + */ + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializes composer proxy state. + * @param _initialOwner Owner address for administrative operations. + * @param _endpoint Trusted endpoint allowed to invoke `lzCompose`. + * @param _trustedSourceOApp Trusted source OApp address. + * @param _assetManager Asset manager used for redemption. + * @param _stableCoin Stable coin token - returned in case of a redemption failure. + * @param _wNat Wrapped native token - returned in case of a redemption failure + * if stable coin balance is insufficient. + * @param _composerFeeRecipient Recipient of composer fee collected in fAsset. + * @param _defaultComposerFeePPM Default composer fee in PPM. + * @param _redeemerAccountImplementation Beacon implementation for redeemer accounts. + */ + function initialize( + address _initialOwner, + address _endpoint, + address _trustedSourceOApp, + IAssetManager _assetManager, + IERC20 _stableCoin, + IERC20 _wNat, + address _composerFeeRecipient, + uint256 _defaultComposerFeePPM, + address _redeemerAccountImplementation + ) external initializer { + require(_initialOwner != address(0), InvalidAddress()); + require(_endpoint != address(0), InvalidAddress()); + require(_trustedSourceOApp != address(0), InvalidAddress()); + require(address(_assetManager).code.length > 0, InvalidAddress()); + require(address(_stableCoin).code.length > 0, InvalidAddress()); + require(address(_wNat).code.length > 0, InvalidAddress()); + require(_composerFeeRecipient != address(0), InvalidAddress()); + require( + _defaultComposerFeePPM < PPM_DENOMINATOR, + InvalidComposerFeePPM() + ); + require( + _redeemerAccountImplementation.code.length > 0, + InvalidRedeemerAccountImplementation() + ); + + __Ownable_init(_initialOwner); - constructor(address _endpoint) Ownable(msg.sender) { endpoint = _endpoint; + trustedSourceOApp = _trustedSourceOApp; + assetManager = _assetManager; + fAsset = _assetManager.fAsset(); + require(address(fAsset).code.length > 0, InvalidAddress()); + stableCoin = _stableCoin; + wNat = _wNat; + composerFeeRecipient = _composerFeeRecipient; + defaultComposerFeePPM = _defaultComposerFeePPM; + redeemerAccountImplementation = _redeemerAccountImplementation; + + emit ComposerFeeRecipientSet(_composerFeeRecipient); + emit DefaultComposerFeeSet(_defaultComposerFeePPM); + emit RedeemerAccountImplementationSet(redeemerAccountImplementation); + } + + /** + * @notice Updates default composer fee in PPM. + * @param _defaultComposerFeePPM New default composer fee in PPM. + */ + function setDefaultComposerFee( + uint256 _defaultComposerFeePPM + ) external onlyOwnerWithTimelock { + require( + _defaultComposerFeePPM < PPM_DENOMINATOR, + InvalidComposerFeePPM() + ); + defaultComposerFeePPM = _defaultComposerFeePPM; + emit DefaultComposerFeeSet(_defaultComposerFeePPM); + } + + /** + * @notice Sets srcEid-specific composer fees in PPM. + * @dev Uses fee+1 storage to distinguish unset (0) from an explicit zero fee. + * @param _srcEids List of OFT source endpoint IDs. + * @param _composerFeesPPM Composer fee values in PPM for corresponding srcEids. + */ + function setComposerFees( + uint32[] calldata _srcEids, + uint256[] calldata _composerFeesPPM + ) external onlyOwnerWithTimelock { + require(_srcEids.length == _composerFeesPPM.length, LengthMismatch()); + + for (uint256 i = 0; i < _srcEids.length; i++) { + uint32 srcEid = _srcEids[i]; + uint256 feePPM = _composerFeesPPM[i]; + require(feePPM < PPM_DENOMINATOR, InvalidComposerFeePPM()); + composerFeesPPM[srcEid] = feePPM + 1; + emit ComposerFeeSet(srcEid, feePPM); + } + } + + /** + * @notice Removes srcEid-specific composer fee overrides. + * @param _srcEids List of OFT source endpoint IDs. + */ + function removeComposerFees( + uint32[] calldata _srcEids + ) external onlyOwnerWithTimelock { + for (uint256 i = 0; i < _srcEids.length; i++) { + uint32 srcEid = _srcEids[i]; + require(composerFeesPPM[srcEid] != 0, ComposerFeeNotSet(srcEid)); + delete composerFeesPPM[srcEid]; + emit ComposerFeeRemoved(srcEid); + } + } + + /** + * @notice Updates recipient for collected composer fee. + * @param _composerFeeRecipient New recipient address. + */ + function setComposerFeeRecipient( + address _composerFeeRecipient + ) external onlyOwnerWithTimelock { + require( + _composerFeeRecipient != address(0), + InvalidComposerFeeRecipient() + ); + composerFeeRecipient = _composerFeeRecipient; + emit ComposerFeeRecipientSet(_composerFeeRecipient); + } + + /** + * @notice Updates beacon implementation used by redeemer accounts. + * @param _implementation New implementation address. + */ + function setRedeemerAccountImplementation( + address _implementation + ) external onlyOwnerWithTimelock { + require( + _implementation.code.length > 0, + InvalidRedeemerAccountImplementation() + ); + redeemerAccountImplementation = _implementation; + emit RedeemerAccountImplementationSet(_implementation); + } + + /** + * @notice Updates executor data used for redemption execution. + * @param _executor New executor address. + * @param _executorFee New expected fee for executor. + */ + function setExecutorData( + address payable _executor, + uint256 _executorFee + ) external onlyOwnerWithTimelock { + require( + _executor != address(0) || _executorFee == 0, + InvalidExecutorData() + ); + executor = _executor; + executorFee = _executorFee; + emit ExecutorDataSet(_executor, _executorFee); } - receive() external payable {} + /** + * @notice Transfers f-assets held by composer to a target address. + * @dev Recovery function for funds stuck on composer when compose flow fails or is not invoked. + * @param _to Recipient address. + * @param _amount Amount of f-asset to transfer. + */ + function transferFAsset( + address _to, + uint256 _amount + ) external onlyOwnerWithTimelock { + require(_to != address(0), InvalidAddress()); + fAsset.safeTransfer(_to, _amount); + emit FAssetTransferred(_to, _amount); + } + /** + * @notice Transfers native tokens held by composer to a target address. + * @dev Recovery function for funds stuck on composer when compose flow fails. + * @param _to Recipient address. + * @param _amount Amount of native tokens to transfer. + */ + function transferNative( + address _to, + uint256 _amount + ) external onlyOwnerWithTimelock { + require(_to != address(0), InvalidAddress()); + (bool success, ) = _to.call{value: _amount}(""); + require(success, NativeTransferFailed()); + emit NativeTransferred(_to, _amount); + } + + /// @inheritdoc ILayerZeroComposer function lzCompose( - address /* _from */, - bytes32 /* _guid */, + address _from, + bytes32 _guid, bytes calldata _message, address /* _executor */, bytes calldata /* _extraData */ - ) external payable override nonReentrant { - if (msg.sender != endpoint) revert OnlyEndpoint(); + ) external payable nonReentrant { + require(msg.sender == endpoint, OnlyEndpoint()); + require(_from == trustedSourceOApp, InvalidSourceOApp(_from)); + + uint32 srcEid = OFTComposeMsgCodec.srcEid(_message); + uint256 amountLD = OFTComposeMsgCodec.amountLD(_message); + uint256 composerFeePPM = _getComposerFeePPM(srcEid); + uint256 composerFee = Math.mulDiv( + amountLD, + composerFeePPM, + PPM_DENOMINATOR + ); + uint256 amountToRedeemAfterFee = amountLD - composerFee; + RedeemComposeData memory data = abi.decode( + OFTComposeMsgCodec.composeMsg(_message), + (RedeemComposeData) + ); + require(data.redeemer != address(0), InvalidAddress()); - bytes memory composeMsg = OFTComposeMsgCodec.composeMsg(_message); - _processRedemption(composeMsg); + if (composerFee > 0) { + fAsset.safeTransfer(composerFeeRecipient, composerFee); + emit ComposerFeeCollected( + _guid, + srcEid, + composerFeeRecipient, + composerFee + ); + } + + address redeemerAccount = _getOrCreateRedeemerAccount(data.redeemer); + fAsset.safeTransfer(redeemerAccount, amountToRedeemAfterFee); + emit FAssetTransferred(redeemerAccount, amountToRedeemAfterFee); + + try + IIFAssetRedeemerAccount(redeemerAccount).redeemFAsset{ + value: msg.value + }( + assetManager, + amountToRedeemAfterFee, + data.redeemerUnderlyingAddress, + executor, + executorFee + ) + returns (uint256 _redeemedAmountUBA) { + emit FAssetRedeemed( + _guid, + srcEid, + data.redeemer, + redeemerAccount, + amountToRedeemAfterFee, + data.redeemerUnderlyingAddress, + executor, + executorFee, + _redeemedAmountUBA + ); + } catch { + emit FAssetRedeemFailed( + _guid, + srcEid, + data.redeemer, + redeemerAccount, + amountToRedeemAfterFee + ); + } } - function _processRedemption(bytes memory composeMsg) internal { - // 1. Decode message - (, string memory underlyingAddress, address redeemer) = abi.decode( - composeMsg, - (uint256, string, address) - ); + /// @inheritdoc IFAssetRedeemComposer + function getComposerFeePPM( + uint32 _srcEid + ) external view returns (uint256 _composerFeePPM) { + _composerFeePPM = _getComposerFeePPM(_srcEid); + } - // 2. Get Asset Manager & fXRP Token from Registry - IAssetManager assetManager = ContractRegistry.getAssetManagerFXRP(); - IERC20 fAssetToken = IERC20(address(assetManager.fAsset())); + /// @inheritdoc IBeacon + function implementation() external view returns (address) { + return redeemerAccountImplementation; + } - // 3. Check Actual Balance received from LayerZero - uint256 currentBalance = fAssetToken.balanceOf(address(this)); - if (currentBalance == 0) revert InsufficientBalance(); + /// @inheritdoc IFAssetRedeemComposer + function getExecutorData() + external + view + returns (address payable _executor, uint256 _executorFee) + { + _executor = executor; + _executorFee = executorFee; + } - // 4. Calculate Lots - uint256 lotSizeUBA = assetManager.getSettings().lotSizeAMG; - uint256 lots = currentBalance / lotSizeUBA; + /// @inheritdoc IFAssetRedeemComposer + function getRedeemerAccountAddress( + address _redeemer + ) external view returns (address _redeemerAccount) { + _redeemerAccount = redeemerToRedeemerAccount[_redeemer]; + if (_redeemerAccount == address(0)) { + bytes memory bytecode = _generateRedeemerAccountBytecode(_redeemer); + _redeemerAccount = Create2.computeAddress( + bytes32(0), + keccak256(bytecode) + ); + } + } - if (lots == 0) revert AmountTooSmall(); + /** + * @inheritdoc UUPSUpgradeable + * @dev Only owner can call this method. + */ + function upgradeToAndCall( + address _newImplementation, + bytes memory _data + ) public payable override onlyOwnerWithTimelock { + super.upgradeToAndCall(_newImplementation, _data); + } - // 5. Calculate amount to burn - uint256 amountToRedeem = lots * lotSizeUBA; + /** + * Unused. Present just to satisfy UUPSUpgradeable requirement as call is timelocked. + * The real check is in onlyOwnerWithTimelock modifier on upgradeToAndCall. + */ + function _authorizeUpgrade(address _newImplementation) internal override {} - // 6. Approve AssetManager to spend the tokens - fAssetToken.forceApprove(address(assetManager), amountToRedeem); + /** + * @notice Gets existing redeemer account or creates a deterministic one. + * @param _redeemer Redeemer account owner address. + * @return _redeemerAccount Redeemer account address. + */ + function _getOrCreateRedeemerAccount( + address _redeemer + ) internal returns (address _redeemerAccount) { + _redeemerAccount = redeemerToRedeemerAccount[_redeemer]; + if (_redeemerAccount != address(0)) { + return _redeemerAccount; + } - // 7. Redeem - uint256 redeemedAmount = assetManager.redeem( - lots, - underlyingAddress, - payable(address(0)) - ); + // redeemer account does not exist, create it + bytes memory bytecode = _generateRedeemerAccountBytecode(_redeemer); + _redeemerAccount = Create2.deploy(0, bytes32(0), bytecode); // reverts on failure + redeemerToRedeemerAccount[_redeemer] = _redeemerAccount; + emit RedeemerAccountCreated(_redeemer, _redeemerAccount); - emit RedemptionTriggered( - redeemer, - underlyingAddress, - redeemedAmount, - lots + // set unlimited allowances for fAsset, stable coin and wNat + // to enable redeemer to transfer funds to redeemer address in case of redemption failure + IIFAssetRedeemerAccount(_redeemerAccount).setMaxAllowances( + fAsset, + stableCoin, + wNat ); } - function recoverTokens( - address token, - address to, - uint256 amount - ) external onlyOwner { - IERC20(token).safeTransfer(to, amount); + /** + * @notice Builds CREATE2 deployment bytecode for redeemer account proxy. + * @param _redeemer Redeemer account owner address. + * @return Bytecode used for deterministic deployment. + */ + function _generateRedeemerAccountBytecode( + address _redeemer + ) internal view returns (bytes memory) { + return + abi.encodePacked( + type(FAssetRedeemerAccountProxy).creationCode, + abi.encode(address(this), _redeemer) + ); } - function recoverNative() external onlyOwner { - payable(owner()).transfer(address(this).balance); + /** + * @notice Retrieves composer fee in PPM for a given srcEid, falling back to default if not set. + * @param _srcEid OFT source endpoint ID. + * @return _composerFeePPM Composer fee in PPM. + */ + function _getComposerFeePPM( + uint32 _srcEid + ) internal view returns (uint256 _composerFeePPM) { + uint256 srcEidFeePlusOne = composerFeesPPM[_srcEid]; + if (srcEidFeePlusOne > 0) { + return srcEidFeePlusOne - 1; + } + + return defaultComposerFeePPM; } }