Repository navigation
feat(FraxFacet): add Frax HopV2 (LayerZero OFT) bridge facet (EXSC-382) [FraxFacet v1.0.0, IFraxHopV2 v1.0.0] - #2048
Conversation
… + 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>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughChangesFraxFacet 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
Estimated code review effort: 4 (Complex) | ~60 minutes Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/Interfaces/IFraxHopV2.sol (1)
72-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the auxiliary interfaces into dedicated files.
Place
ITipFeeManagerandIFraxOFTinsrc/Interfaces/ITipFeeManager.solandsrc/Interfaces/IFraxOFT.sol, respectively.As per coding guidelines, “Solidity interfaces should be placed in separate files with the
Iprefix 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
📒 Files selected for processing (13)
config/clearSigningProposal.jsonconfig/frax.jsondocs/FraxFacet.mdscript/demoScripts/demoFrax.tsscript/deploy/facets/DeployFraxFacet.s.solscript/deploy/facets/UpdateFraxFacet.s.solscript/deploy/resources/deployRequirements.jsonscript/deploy/zksync/DeployFraxFacet.zksync.s.solscript/deploy/zksync/UpdateFraxFacet.zksync.s.solsrc/Facets/FraxFacet.solsrc/Interfaces/IFraxHopV2.soltest/solidity/Facets/FraxFacet.t.soltest/solidity/utils/MockFraxHopV2Tempo.sol
Test Coverage ReportLine Coverage: 90.44% (3437 / 3800 lines) |
…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>
Bot-review triageApplied (pushed):
Rejected (with reason):
Note: |
…-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>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tasks/syncLayerZeroEids.ts (1)
21-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse
readonlyfor properties that should not be modified.As per coding guidelines, use
readonlyfor 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 winReuse the centralized
LIFI_CHAIN_ID_SOLANAconstant.To avoid duplication and maintain a single source of truth, consider importing
LIFI_CHAIN_ID_SOLANAfrom thedemoScriptHelpersmodule 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
📒 Files selected for processing (9)
config/frax.jsonconfig/layerzero.jsonconfig/superset.jsondeployments/arbitrum.diamond.staging.jsondeployments/arbitrum.staging.jsondocs/FraxFacet.mdscript/demoScripts/demoFrax.tsscript/tasks/layerZeroEids.test.tstasks/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>
🤖 GitHub Action: Security Alerts Review 🔍🟢 Dismissed Security Alerts with Comments 🟢 View Alert - File: 🟢 View Alert - File: ✅ No unresolved security alerts! 🎉 |
…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>
There was a problem hiding this comment.
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>
…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>
…-sc-implementation # Conflicts: # config/clearSigningProposal.json
…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>
…ent (audit sujithsomraaj/lifi-frax-facet-v1.0.0#4) 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>
Which Linear task belongs to this PR?
EXSC-382 (sub-task EXSC-383) — Frax bridge SC implementation.
Why did I implement it this way?
FraxFacetbridges 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 callsHopV2.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
EndpointV2Altrejects nativemsg.valueand 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, withPATH_USDas the default gas token) read fromconfig/frax.jsonat 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):
refundRecipientviarefundExcessNative. This is a hot path, not an edge case.decimalConversionRatemultiple and only pulls the floored amount. The facet floors up front, bridges exactly that, returns the dust remainder torefundRecipientin-tx, and emitsLiFiTransferStarted.minAmount= the bridged (floored) amount. The diamond retains zero token and zero native after every bridge (asserted in tests).minAmountLD == amountLD; destination funds go straight to the recipient. No LI.FI contract ever holds funds on the destination.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 torefundRecipient(invariant holds even across a HopV2 proxy upgrade).Plain transfers only.
hasDestinationCall == truereverts (doesNotContainDestinationCalls); no receiver periphery, andCalldataVerificationFacetis unchanged (no compose message to validate on-chain — same asPaxosTransitFacet).Trust assumptions (documented in
docs/FraxFacet.md):dstEid(the LayerZero routing target) is cross-checked againstbridgeData.destinationChainIdvia an owner-governed on-chainchainId → EIDmapping (initFrax/setFraxChainIdToEid/getFraxChainIdToEid, diamond storagecom.lifi.facets.frax, mirroringSupersetFacet): the facet requiresgetFraxChainIdToEid(destinationChainId) == dstEid, revertingUnsupportedChainIdwhen the destination is unmapped andInformationMismatchon eid mismatch. Every destination a route targets must be seeded before it is usable — EIDs live in the sharedconfig/layerzero.json(single source of truth for all LZ facets), from which thefrax.jsonmappingsare generated and applied by theUpdateFraxFacetcut init. This is stricter than the analytics-only model ofAcrossFacetV4/PaxosTransitFacet.EID single-sourcing (config).
config/layerzero.jsonis now the one placechainId → LayerZero EIDis defined;frax.json/superset.jsonmappingsare generated from it (tasks/syncLayerZeroEids.ts) andAcrossV4SwapFacet's hardcoded values are pinned to it byscript/tasks/layerZeroEids.test.ts— so a wrong/stale EID fails CI, not on-chain. The reconciliation caught and corrected two wrong Frax EIDs (sei30300→30280, xLayer30196→30274, which disagreed with the audited AcrossV4 values); unverifiedabstractwas dropped pending confirmation. No facet.solchanged (SupersetFacet/AcrossV4 bytecode unchanged — config/test/docs only).recover(); the facet grants it the standardmaxApproveERC20allowance.transferFrompull; the facet pulls it from the caller viadepositAsset, so the caller must make the fee token available to the diamond. This is my best reversible inference from the verifiedRemoteHopV2Temposource 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 onFraxFacet.sol(the only uncovered lines underforge coverage --ir-minimumare the inline-assembly_getStorage()body and theTIP_FEE_MANAGERbranch that is exercised by both suites — known instrumentation artifacts, same asSupersetFacet). Standard path uses an Arbitrum mainnet fork against the real HopV2 + frxUSD self-OFT with live fee quoting; the Tempo path usesMockFraxHopV2Tempo(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):SupersetFacet—setChainIdToEid/getChainIdToEidshared byte-identical selectors withSupersetFacet, so cutting one facet onto a diamond holding the other silently uninstalled the other's selectors. Renamed tosetFraxChainIdToEid/getFraxChainIdToEid(+ eventFraxChainIdToEidSet); added rule102-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.unusedFeeunderflow — guarded the fee-sweep subtraction so a HopV2 over-pull degrades to "no refund" instead of an arithmetic panic; added regression testtest_Tempo_FeeOverPullDegradesToNoRefundInsteadOfReverting._bridgeDatamemory→calldataon the non-swapstartBridgeTokensViaFrax(swap entrypoint staysmemory— it mutatesminAmount).Checklist before requesting a review
/pr-ready(local CodeRabbit) on this branch and resolved (or explicitly documented) all findings — see.agents/commands/pr-ready.mdChecklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)