Skip to content

feat(FraxFacet): add Frax HopV2 (LayerZero OFT) bridge facet (EXSC-382) [FraxFacet v1.0.0, IFraxHopV2 v1.0.0] - #2048

Merged
0xDEnYO merged 30 commits into
mainfrom
feature/exsc-382-frax-sc-implementation
Aug 20, 2026
Merged

0xDEnYO merged 30 commits into
mainfrom
feature/exsc-382-frax-sc-implementation

Conversation

@0xDEnYO

@0xDEnYO 0xDEnYO commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Which Linear task belongs to this PR?

EXSC-382 (sub-task EXSC-383) — Frax bridge SC implementation.

Why did I implement it this way?

FraxFacet bridges through Frax HopV2, a LayerZero V2 OFT hub-and-spoke bridge (Fraxtal chainId 252 is the hub; every other chain is a spoke sharing one CREATE2 address). It calls HopV2.sendOFT(oft, dstEid, recipient, amountLd, 0, "") for the six Frax OFTs (frxUSD, sfrxUSD, WFRAX, frxETH, sfrxETH, FPI).

One facet source for every chain, incl. Tempo. Tempo's LayerZero EndpointV2Alt rejects native msg.value and charges the messaging fee in a TIP20 ERC20 gas token. Rather than a second contract, the fee mode is selected by a constructor immutable (TIP_FEE_MANAGER, non-zero only on Tempo, with PATH_USD as the default gas token) read from config/frax.json at deploy time. This keeps one @custom:version, one audit surface, and one Deploy script; the standard-chain runtime cost is an immutable read + one branch (≈ free), the only price being a few-k gas of one-time deploy bloat for the unused branch on 26 chains.

Money flow (the crux of the review):

  • Excess native fee — the LI.FI/Frax API quotes the native fee with a ~10% buffer; HopV2 refunds the excess to the caller (the diamond) synchronously, and the facet forwards it to refundRecipient via refundExcessNative. This is a hot path, not an edge case.
  • Dust — HopV2 floors the amount to the OFT's decimalConversionRate multiple and only pulls the floored amount. The facet floors up front, bridges exactly that, returns the dust remainder to refundRecipient in-tx, and emits LiFiTransferStarted.minAmount = the bridged (floored) amount. The diamond retains zero token and zero native after every bridge (asserted in tests).
  • No positive slippage / no intent semantics — minAmountLD == amountLD; destination funds go straight to the recipient. No LI.FI contract ever holds funds on the destination.
  • Tempo — msg.value == 0; the fee token is resolved (TIP_FEE_MANAGER.userTokens(diamond) → PATH_USD), pulled from the caller, approved to the hop, and any unpulled remainder is swept back to refundRecipient (invariant holds even across a HopV2 proxy upgrade).

Plain transfers only. hasDestinationCall == true reverts (doesNotContainDestinationCalls); no receiver periphery, and CalldataVerificationFacet is unchanged (no compose message to validate on-chain — same as PaxosTransitFacet).

Trust assumptions (documented in docs/FraxFacet.md):

  • dstEid (the LayerZero routing target) is cross-checked against bridgeData.destinationChainId via an owner-governed on-chain chainId → EID mapping (initFrax/setFraxChainIdToEid/getFraxChainIdToEid, diamond storage com.lifi.facets.frax, mirroring SupersetFacet): the facet requires getFraxChainIdToEid(destinationChainId) == dstEid, reverting UnsupportedChainId when the destination is unmapped and InformationMismatch on eid mismatch. Every destination a route targets must be seeded before it is usable — EIDs live in the shared config/layerzero.json (single source of truth for all LZ facets), from which the frax.json mappings are generated and applied by the UpdateFraxFacet cut init. This is stricter than the analytics-only model of AcrossFacetV4/PaxosTransitFacet.

EID single-sourcing (config). config/layerzero.json is now the one place chainId → LayerZero EID is defined; frax.json/superset.json mappings are generated from it (tasks/syncLayerZeroEids.ts) and AcrossV4SwapFacet's hardcoded values are pinned to it by script/tasks/layerZeroEids.test.ts — so a wrong/stale EID fails CI, not on-chain. The reconciliation caught and corrected two wrong Frax EIDs (sei 30300→30280, xLayer 30196→30274, which disagreed with the audited AcrossV4 values); unverified abstract was dropped pending confirmation. No facet .sol changed (SupersetFacet/AcrossV4 bytecode unchanged — config/test/docs only).

  • HopV2 is an upgradeable proxy with an admin recover(); the facet grants it the standard maxApproveERC20 allowance.

⚠️ Open item for BE / SC review — Tempo fee funding (EXP-514). On Tempo the diamond must possess the TIP20 fee token for the hop's transferFrom pull; the facet pulls it from the caller via depositAsset, so the caller must make the fee token available to the diamond. This is my best reversible inference from the verified RemoteHopV2Tempo source and needs BE confirmation — it does not change the facet ABI, so it can be adjusted without a redesign.

Testing: test/solidity/Facets/FraxFacet.t.sol — 53 tests, 100% function + effectively full line/statement coverage on FraxFacet.sol (the only uncovered lines under forge coverage --ir-minimum are the inline-assembly _getStorage() body and the TIP_FEE_MANAGER branch that is exercised by both suites — known instrumentation artifacts, same as SupersetFacet). Standard path uses an Arbitrum mainnet fork against the real HopV2 + frxUSD self-OFT with live fee quoting; the Tempo path uses MockFraxHopV2Tempo (rule 400: Tempo's TIP20/endpoint are precompile-backed and cannot be forked). Facet + deploy scripts compile clean under the deploy toolchain (solc 0.8.17 + london).

Follow-ups (out of scope here): SC peer review → BE integration (EXP-514) → external audit → staging then production rollout (multisig + diamondCut + diamond-log update). Aurora is excluded (absent from config/networks.json; needs /add-network) — tracked separately.

Peer review round (@gvladika)

Three review findings resolved (commit 7a19f30bb), plus the staging diamond brought in line with the rename (4daf2f1e9):

  1. Selector collision with SupersetFacet — setChainIdToEid/getChainIdToEid shared byte-identical selectors with SupersetFacet, so cutting one facet onto a diamond holding the other silently uninstalled the other's selectors. Renamed to setFraxChainIdToEid/getFraxChainIdToEid (+ event FraxChainIdToEidSet); added rule 102-facets [CONV:FACET-SELECTORS]. The collision had already fired on arbitrum staging (Superset was at 0 selectors), so I redeployed the renamed FraxFacet (→ 0xCDFE2fc28F6f824F9E619728A52363bd558aB4d0, old selectors removed from the diamond) and re-cut SupersetFacet (→ 0x962125b0b3c2329EAd744a0ad383D37F78C608e9). Loupe-verified: zero selector overlap, both bridges live.
  2. Tempo unusedFee underflow — guarded the fee-sweep subtraction so a HopV2 over-pull degrades to "no refund" instead of an arithmetic panic; added regression test test_Tempo_FeeOverPullDegradesToNoRefundInsteadOfReverting.
  3. _bridgeData memory → calldata on the non-swap startBridgeTokensViaFrax (swap entrypoint stays memory — it mutates minAmount).

Checklist before requesting a review

  • I have performed a self-review of my code
  • This pull request is as small as possible and only tackles one problem
  • I have run /pr-ready (local CodeRabbit) on this branch and resolved (or explicitly documented) all findings — see .agents/commands/pr-ready.md
  • I have added tests that cover the functionality / test the bug
  • For new facets: I have checked all points from this list: https://www.notion.so/lifi/New-Facet-Contract-Checklist-157f0ff14ac78095a2b8f999d655622e
  • I have updated any required documentation

Checklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)

  • I have checked that any arbitrary calls to external contracts are validated and or restricted
  • I have checked that any privileged calls (i.e. storage modifications) are validated and or restricted
  • I have ensured that any new contracts have had AT A MINIMUM 1 preliminary audit conducted on by <company/auditor>

0xDEnYO and others added 4 commits July 13, 2026 13:28
… + deploy scripts (EXSC-382)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ clearSigning (EXSC-382)

- swap entrypoint: reject stray msg.value on Tempo; require final swap output == sendingAssetId
- Tempo: return unpulled fee-token remainder to refundRecipient (invariant holds across hop upgrades)
- docs/FraxFacet.md; regenerate clearSigningProposal.json (both Frax entrypoints)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…C20-fee path (EXSC-382)

- 29 Arbitrum-fork tests (real HopV2 + frxUSD self-OFT): dust floor+refund, excess-native refund,
  diamond-retains-nothing, oft/token mismatch, validation reverts, swap-output guard
- 7 local Tempo tests via MockFraxHopV2Tempo (precompile TIP20 can't fork; rule 400): msg.value==0
  guard, ERC20 fee pull, opted-in fee token, unused-fee sweep

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o guard wording in docs (EXSC-382)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lifi-action-bot
lifi-action-bot marked this pull request as draft July 13, 2026 07:36
@github-actions github-actions Bot added the requires-types Trigger Types Bindings CI (ABI/type generation for lifi-contract-types) label Jul 13, 2026
@lifi-action-bot lifi-action-bot changed the title feat(FraxFacet): add Frax HopV2 (LayerZero OFT) bridge facet (EXSC-382) feat(FraxFacet): add Frax HopV2 (LayerZero OFT) bridge facet (EXSC-382) [FraxFacet v1.0.0, IFraxHopV2 v1.0.0] Jul 13, 2026
Comment thread src/Facets/FraxFacet.sol Fixed
Comment thread src/Facets/FraxFacet.sol Fixed
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

FraxFacet adds standard and Tempo Frax HopV2 bridging with direct and swap-assisted entrypoints, destination routing validation, dust and fee refunds, deployment configuration, LayerZero mapping synchronization, tests, documentation, a runnable demo, and clear-signing metadata.

Frax bridging integration

Layer / File(s) Summary
FraxFacet bridge contract
src/Facets/FraxFacet.sol, src/Interfaces/IFraxHopV2.sol
Adds Frax HopV2 interfaces and standard or Tempo bridging entrypoints with validation, dust handling, refunds, and swap support.
Configuration and deployment wiring
config/frax.json, script/deploy/..., script/deploy/resources/deployRequirements.json, deployments/*.json
Adds chain-specific Frax addresses, deployment/update scripts, and staging deployment entries.
LayerZero mapping synchronization
config/layerzero.json, tasks/syncLayerZeroEids.ts, script/tasks/layerZeroEids.test.ts, config/superset.json
Adds the authoritative EID mapping, deterministic mapping generation, Superset configuration updates, and consistency tests.
Tempo mock environment
test/solidity/utils/MockFraxHopV2Tempo.sol
Adds mock OFT, fee manager, ERC20 behavior, HopV2 fee quoting, dust flooring, and token transfers.
Bridge and Tempo test coverage
test/solidity/Facets/FraxFacet.t.sol
Tests fork-based bridging, swaps, validation failures, dust and native refunds, constructor checks, and Tempo ERC20 fee handling.
Demo, signing metadata, and documentation
script/demoScripts/demoFrax.ts, config/clearSigningProposal.json, docs/FraxFacet.md
Adds a staging Frax bridge demo, signing definitions for both entrypoints, and documentation with route, fee, validation, and quote examples.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested labels: AuditRequired, requires-types

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding the Frax HopV2 bridge facet.
Description check ✅ Passed The description follows the template and includes the task, rationale, and checklists.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/exsc-382-frax-sc-implementation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/Interfaces/IFraxHopV2.sol (1)

72-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the auxiliary interfaces into dedicated files.

Place ITipFeeManager and IFraxOFT in src/Interfaces/ITipFeeManager.sol and src/Interfaces/IFraxOFT.sol, respectively.

As per coding guidelines, “Solidity interfaces should be placed in separate files with the I prefix naming convention.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Interfaces/IFraxHopV2.sol` around lines 72 - 92, The auxiliary interfaces
ITipFeeManager and IFraxOFT should not remain in IFraxHopV2.sol. Move each
interface into its own dedicated file, src/Interfaces/ITipFeeManager.sol and
src/Interfaces/IFraxOFT.sol, preserving their declarations and documentation,
and update any imports or references to use the new locations.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/clearSigningProposal.json`:
- Around line 806-872: Update both Frax clear-signing entries to display
_fraxData.dstEid as the destination and _fraxData.refundRecipient as the refund
recipient, replacing the descriptive _bridgeData.destinationChainId presentation
where appropriate. Add visible field definitions for both _fraxData paths in
each entry, using suitable destination and address formatting while preserving
the existing bridge fields.

In `@docs/FraxFacet.md`:
- Around line 127-134: Update the “LiFi Data” documentation to clarify that
BridgeData is not strictly for analytics: explain that fields such as
sendingAssetId, receiver, and minAmount affect transfer behavior, while limiting
the analytics description to metadata such as transaction ID, integrator, and
referrer.

In `@script/demoScripts/demoFrax.ts`:
- Around line 243-283: Replace the warning-only sanity checks in the quote
mapping flow with validation that aborts before sending funds when any external
quote field mismatches expectations: oft, dstEid, recipient, amountLd,
approvalAddress, or transactionRequest.to. Derive the expected Hop target as
FRAX_HOP_SPOKE for both routes because Arbitrum is always the source, and
compare addresses and numeric values using appropriate normalized
representations. Preserve the existing API-derived values only after all
validations pass.

In `@src/Facets/FraxFacet.sol`:
- Line 229: Update the Tempo fee refund logic surrounding _sendViaTempo in
FraxFacet so the refund baseline accounts for the bridged token when feeToken
equals _bridgeData.sendingAssetId. Exclude _amount from the pre-snapshot or
otherwise track only the fee-funded deposit, preventing balanceOf(address(this))
- feeTokenBalanceBefore from underflowing after sendOFT transfers the bridged
tokens.

---

Nitpick comments:
In `@src/Interfaces/IFraxHopV2.sol`:
- Around line 72-92: The auxiliary interfaces ITipFeeManager and IFraxOFT should
not remain in IFraxHopV2.sol. Move each interface into its own dedicated file,
src/Interfaces/ITipFeeManager.sol and src/Interfaces/IFraxOFT.sol, preserving
their declarations and documentation, and update any imports or references to
use the new locations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 93c6ffc8-add5-49b0-9067-b1b2bb107c25

📥 Commits

Reviewing files that changed from the base of the PR and between 6949564 and 42bfa17.

📒 Files selected for processing (13)
  • config/clearSigningProposal.json
  • config/frax.json
  • docs/FraxFacet.md
  • script/demoScripts/demoFrax.ts
  • script/deploy/facets/DeployFraxFacet.s.sol
  • script/deploy/facets/UpdateFraxFacet.s.sol
  • script/deploy/resources/deployRequirements.json
  • script/deploy/zksync/DeployFraxFacet.zksync.s.sol
  • script/deploy/zksync/UpdateFraxFacet.zksync.s.sol
  • src/Facets/FraxFacet.sol
  • src/Interfaces/IFraxHopV2.sol
  • test/solidity/Facets/FraxFacet.t.sol
  • test/solidity/utils/MockFraxHopV2Tempo.sol

Comment thread config/clearSigningProposal.json Outdated
Comment thread docs/FraxFacet.md
Comment thread script/demoScripts/demoFrax.ts
Comment thread src/Facets/FraxFacet.sol Outdated
@lifi-action-bot

lifi-action-bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Collaborator

Test Coverage Report

Line Coverage: 90.44% (3437 / 3800 lines)
Function Coverage: 93.68% ( 519 / 554 functions)
Branch Coverage: 75.76% ( 672 / 887 branches)
Test coverage (90.44%) is above min threshold (89%). Check passed.

…ing mismatch; docs clarify enforced BridgeData fields (EXSC-382)

- demoFrax: source-chain Hop is always the arbitrum spoke for both routes; validate oft/dstEid/recipient/amount/approvalAddress/to and throw on mismatch instead of warn
- docs: BridgeData is not strictly analytics — sendingAssetId/receiver/minAmount are enforced

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@0xDEnYO

0xDEnYO commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Bot-review triage

Applied (pushed):

  • CodeRabbit — demoFrax.ts routing: fixed. The source-chain Hop is always the arbitrum spoke for both routes (arbitrum is the source), and the demo now validates oft/dstEid/recipient/amount/approvalAddress/transactionRequest.to and throws on mismatch instead of warning.
  • CodeRabbit — docs "BridgeData is strictly analytics": fixed. Clarified that sendingAssetId/receiver/minAmount are enforced; only transactionId/integrator/referrer/destinationChainId are descriptive.

Rejected (with reason):

  • CodeRabbit — show _fraxData.dstEid/refundRecipient in the clear-signing entries: config/clearSigningProposal.json is generated by tasks/buildClearSigningProposal.ts, which emits only the standard _bridgeData/_swapData descriptors — no facet displays facet-specific struct fields. A manual edit would be inconsistent with every other facet, get overwritten on regeneration, and fail the verify-clear-signing CI that asserts committed == generated. The clear-signing surface is intentionally generator-driven.
  • Olympix — "reentrancy: event emitted after external call" (FraxFacet.sol, LiFiTransferStarted): intentional and mandated by [CONV:EVENTS] (emit at the end of _startBridge after the bridge call), and the entrypoints are nonReentrant. Same pattern as every LI.FI facet.
  • Olympix — "unsafe downcast": false positive. The flagged expression is bytes32(uint256(uint160(receiver))) — all widening; there is no narrowing cast in the facet.

Note: audit-verification failing + the auto-draft / AuditRequired label are the expected new-facet gate; the external audit is a downstream step (SC review → BE → audit → prod rollout).

0xDEnYO and others added 5 commits July 20, 2026 10:43
…-sc-implementation

# Conflicts:
#	config/clearSigningProposal.json
…; harden fee-token + modifier consistency (EXSC-382)

- add owner-governed chainId -> LayerZero EID diamond-storage mapping
  (initFrax / setChainIdToEid / getChainIdToEid), mirroring SupersetFacet, and
  cross-check bridgeData.destinationChainId against fraxData.dstEid (reverts
  UnsupportedChainId when unmapped, InformationMismatch on eid mismatch)
- reject the Tempo fee-token == bridged-token collision (InformationMismatch) so
  the unused-fee balance-delta accounting cannot strand the bridged amount
- align swapAndStartBridgeTokensViaFrax modifier order with the non-swap entrypoint
  (validateBridgeData first)
- seed config/frax.json mappings with verified LayerZero EIDs + a DEV NOTICE; wire
  initFrax into the Update deploy scripts (EVM + zksync) as the diamondCut init call
- tests: +16 (mapping admin, destinationChainId mismatch/unsupported, fee-token
  collision); docs updated

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on (EXSC-382)

- add config/layerzero.json as the one source of truth for chainId -> LayerZero
  EID, built from AcrossV4SwapFacet's audited values + fork-verified fraxtal
- tasks/syncLayerZeroEids.ts generates the `mappings` arrays in frax.json /
  superset.json from it (subset = each facet's own supported networks: hop /
  poolManager); deploy scripts still read `.mappings` unchanged
- script/tasks/layerZeroEids.test.ts pins all three consumers (frax, superset,
  AcrossV4's hardcoded _chainIdToLzEid) to layerzero.json so a wrong/stale EID
  fails in CI instead of on-chain
- fix two wrong Frax EIDs surfaced by the reconciliation: sei 30300 -> 30280,
  xLayer 30196 -> 30274 (they disagreed with the audited AcrossV4 values); drop
  unverified abstract until its EID is confirmed
- no facet .sol changes (SupersetFacet/AcrossV4SwapFacet bytecode unchanged);
  docs updated

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deployed FraxFacet at 0x3B76d15Eb68a7a0E09b86D9aee3c49fD006cdFB2 (CREATE3) and
diamondCut into the arbitrum staging LiFiDiamond (0xD3b2b0aC0AFdd0d166a495f5E9fca4eCc715a782);
initFrax seeded the chainId->LayerZero-EID mapping (verified on-chain, e.g.
getChainIdToEid(252)=30255). Enables the demoFrax.ts staging smoke test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (EXSC-382)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tasks/syncLayerZeroEids.ts (1)

21-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use readonly for properties that should not be modified.

As per coding guidelines, use readonly for interface properties that do not require reassignment after initialization.

💡 Proposed refactor
 export interface IChainIdConfig {
-  chainId: number
-  lzEid: number
+  readonly chainId: number
+  readonly lzEid: number
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tasks/syncLayerZeroEids.ts` around lines 21 - 24, Update the IChainIdConfig
interface so its chainId and lzEid properties are readonly, preventing
reassignment after initialization.

Source: Coding guidelines

script/tasks/layerZeroEids.test.ts (1)

25-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the centralized LIFI_CHAIN_ID_SOLANA constant.

To avoid duplication and maintain a single source of truth, consider importing LIFI_CHAIN_ID_SOLANA from the demoScriptHelpers module instead of redefining it as a literal here.

♻️ Proposed refactor
 } from '../../tasks/syncLayerZeroEids'
+import { LIFI_CHAIN_ID_SOLANA } from '../demoScripts/utils/demoScriptHelpers'
 
 const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '../..')
-const LIFI_CHAIN_ID_SOLANA = '1151111081099710'

Additionally, update the test assertion around line 66 since the imported constant is a BigInt:

    expect(eids[LIFI_CHAIN_ID_SOLANA.toString()]).toBe(Number(solana[1]))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/tasks/layerZeroEids.test.ts` around lines 25 - 27, Remove the local
LIFI_CHAIN_ID_SOLANA literal and import the centralized constant from
demoScriptHelpers. In the test assertion using this key, convert the imported
BigInt to a string for eids lookup while preserving the numeric comparison
against solana[1].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@script/tasks/layerZeroEids.test.ts`:
- Around line 25-27: Remove the local LIFI_CHAIN_ID_SOLANA literal and import
the centralized constant from demoScriptHelpers. In the test assertion using
this key, convert the imported BigInt to a string for eids lookup while
preserving the numeric comparison against solana[1].

In `@tasks/syncLayerZeroEids.ts`:
- Around line 21-24: Update the IChainIdConfig interface so its chainId and
lzEid properties are readonly, preventing reassignment after initialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883efe7b-862a-4a80-a182-b652462a4a65

📥 Commits

Reviewing files that changed from the base of the PR and between 9efefa0 and 047a4d6.

📒 Files selected for processing (9)
  • config/frax.json
  • config/layerzero.json
  • config/superset.json
  • deployments/arbitrum.diamond.staging.json
  • deployments/arbitrum.staging.json
  • docs/FraxFacet.md
  • script/demoScripts/demoFrax.ts
  • script/tasks/layerZeroEids.test.ts
  • tasks/syncLayerZeroEids.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • config/frax.json
  • script/demoScripts/demoFrax.ts
  • docs/FraxFacet.md

…-382)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@0xDEnYO
0xDEnYO marked this pull request as ready for review July 20, 2026 06:27
@lifi-action-bot

lifi-action-bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 GitHub Action: Security Alerts Review 🔍

🟢 Dismissed Security Alerts with Comments
The following alerts were dismissed with proper comments:

🟢 View Alert - File: src/Facets/FraxFacet.sol
🔹 Performing a narrowing downcast may result in silent overflow due to bit truncation. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/unsafe-downcast
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: Not a narrowing downcast: address(160b)->uint160 is same-width (no truncation), then widened to uint256 and reinterpreted as bytes32. Standard LI.FI address->bytes32 recipient encoding; no value can be truncated.

🟢 View Alert - File: src/Facets/FraxFacet.sol
🔹 Reentrant functions which emit events after making an external call may lead to out-of-order events. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/reentrancy-events
🔹 Dismiss Reason: Won't fix
🔹 Dismiss Comment: Intentional and safe: the function is nonReentrant, and emitting LiFiTransferStarted at the end of _startBridge (after the external bridge call) is the mandated CONV:EVENTS pattern used by every LI.FI facet. The reentrancy guard prevents any out-of-order-event scenario.

✅ No unresolved security alerts! 🎉

@lifi-action-bot
lifi-action-bot marked this pull request as draft July 20, 2026 06:28
Comment thread tasks/syncLayerZeroEids.ts Outdated
@0xDEnYO
0xDEnYO marked this pull request as ready for review July 20, 2026 07:09
@0xDEnYO
0xDEnYO enabled auto-merge (squash) July 20, 2026 07:09
0xDEnYO and others added 3 commits July 20, 2026 17:34
…ionalConfigContractAddress (EXSC-382)

frax.json's tipFeeManager/pathUsd were all-zero on 26 of 27 networks (non-zero
only on Tempo). Explicit all-zero maps are pure noise and a footgun: the strict
config reader reverts on a MISSING key, so the first FraxFacet deploy on any
future network (added via /add-network, which never touches bridge configs)
would revert on a value that was always meant to be zero.

- Add _getOptionalConfigContractAddress to both ScriptBase files: returns
  address(0) when the key is absent (stdJson.keyExists guard), no zero/contract
  check on present values (Tempo's precompiles have no bytecode).
- Sparsify tipFeeManager/pathUsd in frax.json to list only Tempo; the DEV NOTICE
  documents the sparse convention.
- Switch both DeployFraxFacet scripts to the optional reader.
- Codify the required-vs-optional sparsity rule in 004-config-structure so future
  configs are created in the optimal shape.

The bash checkDeployRequirements layer already tolerates a missing key when
allowToDeployWithZeroAddress is "true" (already set for both keys), so the deploy
pipeline is unaffected end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n + sync task (EXSC-382)

Revert the EID single-sourcing indirection: the chainId -> LayerZero EID
mappings live directly in each facet's config again, as hand-maintained
arrays. Removes config/layerzero.json, tasks/syncLayerZeroEids.ts, and the
script/tasks/layerZeroEids.test.ts drift guard. The mapping DATA (including the
earlier sei/xLayer EID corrections and the dropped-until-verified abstract) is
kept as-is in config/frax.json / config/superset.json — only the "generated"
tooling and notices are removed.

No .sol changes; deploy scripts already read `.mappings` directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(EXSC-382)

Abstract is listed under `hop` but was dropped from `mappings` pending EID
verification. EID 30324 confirmed against the LayerZero deployments list
(https://docs.layerzero.network/v2/deployments/deployed-contracts), so routes
to Abstract can now be seeded. Matches AcrossV4SwapFacet where present (Across
does not list Abstract, so no cross-facet conflict).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifi-qa-agent[bot]
lifi-qa-agent Bot previously approved these changes Aug 3, 2026

@lifi-qa-agent lifi-qa-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA Pass confirmed — merge-from-main (36fff99) is non-substantive for FraxFacet; selector collision re-checked against SupersetFacet v1.1.0 (no new collision); clearSigningProposal.json FraxFacet entries intact. AuditRequired remains in force.

…n-EVM (Solana) receivers, approvedOft pre-check (EXSC-382) (#2138)

* fix(FraxFacet): address Frax external review — deprecated spokes, EVM-only receiver, approvedOft pre-check (EXSC-382)

Addresses the external review from the Frax team on PR #2048.

Blocking fix
- config/frax.json: remove berachain, mode and scroll from the "hop" address map
  and drop the mode (34443) / scroll (534352) "mappings" entries — Frax is
  deprecating those spokes from the hop mesh. DEV NOTICE records why so they do
  not get re-added.

Hardening
- EVM-only receiver: reject bridgeData.receiver == NON_EVM_ADDRESS with
  InvalidReceiver. HopV2's recipient is bytes32 and Frax routes to Solana
  (EID 30168) via the Fraxtal hub, but this version derives the recipient from the
  20-byte receiver, so the sentinel would otherwise be bridged literally and the
  funds delivered to 0x11f1...f1. The inverse (non-EVM destination with an EVM
  receiver) is already covered: no non-EVM chain ID is in "mappings", so
  getFraxChainIdToEid reverts UnsupportedChainId. Adding a bytes32 non-EVM
  receiver to FraxData is deliberately left as a scoped follow-up.
- HOP.approvedOft(oft) pre-check (added to IFraxHopV2, verified live on both the
  Fraxtal hub and the Arbitrum spoke): an OFT the hop does not route now reverts
  TokenNotSupported in _validateFraxData. Moved the oft.token() ==
  sendingAssetId check there too, so both OFT invariants are enforced before any
  deposit or swap rather than after _depositAndSwap. Both paths revert atomically
  either way — this is a gas/error-clarity win, not a fund-safety one.
- Re-pinned the Arbitrum fork block 483300000 -> 488500000 so the fork tests run
  against the upgraded RemoteHopV2 implementation. All tests pass.
- docs/FraxFacet.md: document the approval trust assumption explicitly —
  LibAsset.maxApproveERC20 sets the allowance to type(uint256).max (the amount
  argument is only the required-allowance threshold), so the diamond carries a
  standing unlimited allowance to HopV2, an upgradeable proxy. Corrects the prior
  "scoped to the bridged amount" wording. Also documents the EVM-only scoping, the
  approvedOft pre-check, the deprecated spokes and the source-only chains
  (katana/somnia/stable/tempo have a hop but no destination mapping).

Tests: 6 new (non-EVM sentinel and unapproved-OFT rejection on both entry points,
plus both on the Tempo branch); 59 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(FraxFacet): full non-EVM (Solana) support, on-chain mapping removal, fork-only tests (EXSC-382)

Follow-up to the external-review round, addressing Daniel's review of #2138.

removeFraxChainIdToEid — deprecation is now enforceable
- initFrax copies config/frax.json mappings into diamond storage, so dropping a chain
  from config did nothing to diamonds already seeded: mode (34443) and scroll (534352)
  are still live on the Arbitrum staging diamond (verified on-chain). Both write paths
  reject lzEid == 0 and no removal path existed, so a seeded destination was permanent.
- Adds owner-only removeFraxChainIdToEid(uint256[]) + FraxChainIdToEidRemoved. Reverts
  UnsupportedChainId on an unset entry so a mistyped removal fails loudly. Docs record
  that retiring a spoke is a two-step op (config + on-chain call) and that staging still
  needs the call.

approvedOft before oft.token()
- oft is caller-supplied, so token() was the first external call the facet made into an
  unvalidated address. Reordered: the hop's allowlist now gates it.

Full non-EVM (Solana) support — replaces the EVM-only scoping
- Solana is live on the hop today: quote(frxUSD, 30168, ...) returns a fee from the
  Arbitrum spoke (a bogus EID reverts), so the previous "unreachable" framing was wrong.
- FraxData gains bytes32 nonEVMReceiver, passed to sendOFT verbatim, with
  BridgeToNonEVMChainBytes32 emitted. Sentinel and destination kind are cross-checked in
  BOTH directions: a 20-byte receiver on a non-EVM route would left-pad into an
  unspendable Solana account, and the sentinel on an EVM route would deliver to
  NON_EVM_ADDRESS. A populated nonEVMReceiver on an EVM route reverts rather than being
  silently ignored.
- Non-EVM destinations are identified by _isNonEVMDestination (LIFI_CHAIN_ID_SOLANA);
  config seeds 1151111081099710 -> 30168. Ripples: clearSigningProposal.json struct
  signatures and demoFrax.ts updated for the new field.

Fork-first tests (rule 400 [CONV:FORK-FIRST]) — no mocked counterparty
- Unapproved OFT: real Arbitrum USDC. It has no token(), so reverting TokenNotSupported
  rather than a failed decode also proves approvedOft is evaluated first.
- token() mismatch: real sfrxUSD OFT (hop-approved, underlying != frxUSD) instead of
  MockFraxOFT.
- Solana routes bridge through the real hop on both entry points.
- Dropped the mock-toggle approvedOft test; the Tempo mock now returns true unconditionally
  and MockFraxOFT is confined to the non-forkable Tempo suite.

Version stays 1.0.0 (still iterating on the facet's first version).

Tests: 72 pass, 0 fail (was 59).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(FraxFacet): rename to unsetFraxChainIdToEid and regenerate clear-signing proposal (EXSC-382)

Fixes the verify-clear-signing failure on #2138. Two distinct mistakes in the
previous commit:

1. removeFraxChainIdToEid failed the clear-signing generator's classifier:
   "unrecognized prefix — doesn't match any classifier and isn't in the explicit
   non-user-facing skip-list". Rather than adding a generator exception, renamed it
   to unsetFraxChainIdToEid (event FraxChainIdToEidUnset) to match the existing
   `unset` prefix, documented in NON_USER_FACING_PREFIXES as "inverse of set*" and
   already used by PolymerCCTPFacet.unsetChainIdToDomainId for the identical
   operation. No shared-tooling change needed. Batch input and the loud
   UnsupportedChainId on an unset entry are kept.

2. config/clearSigningProposal.json is GENERATED by
   tasks/buildClearSigningProposal.ts (plain JSON.stringify, and lint-staged does not
   format *.json). It had been hand-edited and then run through prettier, which
   compacted arrays and would have failed the generator's `git diff --exit-code`
   check even after fix 1. Regenerated from the ABI: the diff against the base branch
   is now exactly the two Frax struct signatures, and re-running the generator is
   byte-idempotent.

Tests: 72 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(clearSigning): expose FraxData.nonEVMReceiver to signers (EXSC-382)

Addresses CodeRabbit on #2138: adding nonEVMReceiver to the ABI tuple without
exposing it in `fields` left signers unable to verify a Solana recipient —
_bridgeData.receiver is only the NON_EVM_ADDRESS sentinel on those routes, so the
one field a signer could see carried no information about the destination.

Fixed in the generator, not the generated file. CodeRabbit's suggested remedy was to
hand-edit config/clearSigningProposal.json lines 737/3750, but that file is emitted by
tasks/buildClearSigningProposal.ts and the exposure is driven by the
BRIDGE_EXTRA_RECEIVERS registry, which had no Frax entry. Added Frax as TYPE-B
(nonEVMReceiver-style, 0x0 on EVM) alongside Mayan/LayerSwap/Chainflip/Eco/
NEARIntents/PolymerCCTP, and regenerated. Both Frax formats now carry
`_fraxData.nonEVMReceiver | "Non-EVM Recipient" | raw | visible: always`, identical to
the other nine facets that expose a non-EVM recipient.

FraxFacet is a slightly stronger case for TYPE-B than its peers: it *enforces*
nonEVMReceiver == bytes32(0) on EVM destinations (reverting InvalidCallData), so the
"0x0 reads as N/A" property is guaranteed rather than conventional. Noted at the
registry entry.

Also synced the TYPE-B list in docs/ClearSigningProposal.md.

Generator output verified byte-idempotent; the generated file was NOT run through
prettier this time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(FraxFacet): pin the sendOFT recipient with expectCall on both branches (EXSC-382)

- assert the exact bytes32 recipient forwarded to HOP.sendOFT on the Solana
  path (both entry points) and the left-padded receiver on the EVM path;
  the emitted event reads fraxData directly, so a mis-wired recipient
  ternary was previously invisible to every test (mutation-verified)
- add the LiFiTransferStarted expectEmit and native-balance check to the
  swap-path Solana test, matching its non-swap sibling
- docs: destinationChainId is enforced (bound to dstEid), not metadata

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…-sc-implementation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/Facets/FraxFacet.sol Dismissed
Comment thread src/Facets/FraxFacet.sol Fixed
gvladika and others added 2 commits August 10, 2026 10:37
…s32 (EXSC-382)

Replace the hand-rolled bytes32(uint256(uint160(receiver))) with
LibBytes.toBytes32 per [CONV:ADDR-BYTES32]. Byte-identical widening cast;
centralises the Olympix unsafe-downcast finding in LibBytes instead of a
per-call-site dismissal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redeploy on arbitrum (bytecode changed since prior staging deploy) and
new deploy on base; CREATE3 address 0x401Aaf6A650A17677821E665838367EBD5bE77BD
on both, registered via diamondCut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifi-qa-agent[bot]
lifi-qa-agent Bot previously approved these changes Aug 10, 2026

@lifi-qa-agent lifi-qa-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA Pass (post-pass re-review) — PR #2138 merge confirmed carries Run #30 approval; LibBytes.toBytes32 refactor is semantically equivalent to prior cast (verified against library source). No new issues. AuditRequired gate and requires-types CI remain active and own their own enforcement.

…-sc-implementation

# Conflicts:
#	config/clearSigningProposal.json
gvladika
gvladika previously approved these changes Aug 12, 2026
gvladika and others added 7 commits August 17, 2026 12:39
…mraaj/lifi-frax-facet-v1.0.0#3)

Rename public immutables HOP/TIP_FEE_MANAGER/PATH_USD to
FRAX_HOP/FRAX_TIP_FEE_MANAGER/FRAX_PATH_USD so their auto-generated
diamond getter selectors carry the facet identifier, per
[CONV:FACET-SELECTORS]. Generic selectors risk a silent cross-facet
collision in UpdateScriptBase.buildDiamondCut, which reassigns a
colliding selector and removes the previously-owning facet's other
selectors. Updates the facet test getter assertions and doc references.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t pre-transfer (audit sujithsomraaj/lifi-frax-facet-v1.0.0#4)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…requirement (audit sujithsomraaj/lifi-frax-facet-v1.0.0#4)

State the caller approve/no-pretransfer requirement as the contract fact it is;
demote EXP-514 to the operational tracking pointer it actually is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… on-chain quote (audit sujithsomraaj/lifi-frax-facet-v1.0.0#1)

On Tempo the facet quoted the TIP20 fee on-chain via HopV2.quoteStatic and pulled
that uncapped amount from the caller. Mirror the native-fee path instead: pull the
caller-supplied FraxData.nativeFee, approve HopV2, and let HopV2 requote and pull its
actual fee. depositAsset pulls exactly nativeFee from the caller, so that is the most
the caller can be charged regardless of HopV2's quote.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Sujith Somraaj independent security review (audit20260820) for FraxFacet
v1.0.0 and IFraxHopV2 v1.0.0, audited at commit 3ae7e5f.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iation

# Conflicts:
#	audit/auditLog.json
#	config/clearSigningProposal.json
Frax destinationChainId entries now use the chainId display format the current
generator emits, matching tasks/buildClearSigningProposal.ts output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@0xDEnYO
0xDEnYO merged commit e3b5ccb into main Aug 20, 2026
49 of 50 checks passed
@0xDEnYO
0xDEnYO deleted the feature/exsc-382-frax-sc-implementation branch August 20, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AuditCompleted AuditRequired requires-types Trigger Types Bindings CI (ABI/type generation for lifi-contract-types)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants