Skip to content

feat(PaxosTransitFacet): Paxos Transit bridge facet (EXSC-547) [PaxosTransitFacet v1.0.0, IPaxosTransit v1.0.0]#1996

Open
0xDEnYO wants to merge 29 commits into
mainfrom
feature/exsc-547-paxos-transit-facet
Open

feat(PaxosTransitFacet): Paxos Transit bridge facet (EXSC-547) [PaxosTransitFacet v1.0.0, IPaxosTransit v1.0.0]#1996
0xDEnYO wants to merge 29 commits into
mainfrom
feature/exsc-547-paxos-transit-facet

Conversation

@0xDEnYO

@0xDEnYO 0xDEnYO commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Which Linear task belongs to this PR?

EXSC-547

Why did I implement it this way?

Implements PaxosTransitFacet — a bridge facet for Paxos Transit (ETH ↔ Robinhood Chain via a Paxos-signed quote), plus 100%-covered unit tests, deploy scaffolding, docs, and a runnable local demo.

Design (per Paxos' answers to our integration questionnaire):

  • The Diamond is the payer. Paxos confirmed submitOrder's msg.sender always provides the offer asset, and the want asset goes to quote.receiver regardless of submitter. So the facet keeps our normal funds flow: deposit/swap into the Diamond → maxApproveERC20 the station → submitOrder, which pulls the offer asset from the Diamond. quote.receiver (validated == bridgeData.receiver) directs the output to the end user. No submitOrderWithPermit/on-behalf call is needed — the Quote struct has no payer field, only receiver. Modeled on GlacisFacet.
  • ERC20-only offer asset (noNativeAsset); msg.value carries the LayerZero messaging fee (nativeFee), forwarded as submitOrder{value: nativeFee}.
  • Locked rate, no slippage param (Paxos rate is fixed by the signed quote). The swap variant requires swap output ≥ quote.offerAmount, refunds positive slippage, and bridges exactly offerAmount.
  • All user-owned value routes to an explicit refundRecipient (review feedback from @gvladika): swap leftovers, positive slippage, and excess source-side native — including LZ fee overage, since the TransitStation forwards the whole msg.value to the LZ endpoint with refundAddress = msg.sender, so the endpoint refunds overage to the Diamond mid-call on essentially every tx (the backend must buffer against fee drift). msg.sender may be a relayer or the Permit2Proxy (where a native refund would strand as owner-recoverable dust), so nothing refunds to msg.sender anymore. Both entrypoints zero-check refundRecipient (InvalidCallData) — without the guard a zero recipient would revert only when fee drift actually left an excess (LibAsset.transferNativeAsset reverts on address(0), it does not burn), i.e. data-dependently and late. Mirrors SupersetFacet's refundAddress; codified as [CONV:FACET-REFUNDS] in .agents/rules/102-facets.md. Known accepted footgun: a refundRecipient that rejects native reverts the bridge (self-inflicted; same as Superset).
  • Quote consistency check in _startBridge: sendingAssetId/minAmount/receiver must match the signed quote, and distributorCode == "LIFI" (0x4c49464900…) for attribution; else InformationMismatch.
  • destEID/wantAsset are trusted from the signed quote (not cross-checked against destinationChainId) — funds always follow the signed quote, same trust model as AcrossFacetV4's outputAmount. Documented in code + docs.

Verified against the live deployment. Paxos deployed + verified the TransitStation on 2026-06-29 at 0x49AAA987b1a7e9E4AE091dcD8332c39F322D7d28 on Ethereum (1) and Robinhood Chain (4663). I checked the verified source + on-chain state and confirmed: submitOrder pulls the offer asset from msg.sender (so the Diamond is the payer — no on-behalf/permit needed); its capability is public (Authority.canCall → true, the Diamond can call it); the on-chain ABI matches IPaxosTransit exactly (exercised directly — the test suite calls the deployed station on a mainnet fork); and no failure path refunds the caller. Real addresses are now in config/paxosTransit.json.

Verification: 39 tests, 100% facet coverage (lines/statements/branches/functions), now run as mainnet-fork tests against the real TransitStation per [CONV:FORK-FIRST] — no mock. (The rule itself ships in this PR: .agents/rules/400-solidity-tests.md gains the fork-first section — mocks are a last resort behind gate-lifting on the fork.) The station's Paxos-signer gate is lifted on the fork (owner-pranked setQuoteSigner to a test key; quotes EIP-712-signed in-test via the new TestPaxosTransitBackendSig helper against the station's live domain), the LZ fee comes from the station's own quoteSend, and the fork is pinned to a block where Paxos has the USDC→USDG route to EID 30416 configured (guarded in setUp). This adds real-behavior coverage a mock couldn't provide: protocolFee/integratorFee/net splits to the real Paxos recipients, replay guard (SignatureAlreadyUsed + usedDigests), QuoteExpired, InvalidSigner, LZ_InsufficientFee on underpaid fee, and LZ fee-overage refunds flowing back to refundRecipient. MockTransitStation is deleted; the demo (demoPaxosTransit.ts) now runs the same real-station flow end-to-end on an anvil mainnet fork (verified locally).

Operational dependencies (Paxos / backend, not this facet): the route (destEID, offerAsset, wantAsset) must be approved by Paxos (setRouteApprovals, routes go live ~2026-07-01); quotes must be backend-signed. Out of scope: on-chain deployment; backend integration.

Reviewed locally with an adversarial code-review pass (funds-safety / correctness) and a self-review pass. Local /pr-ready (CodeRabbit CLI) ran on the latest push: two findings were against an untracked local dev script that is not part of this PR; the one in-scope suggestion is deferred to reviewers below.

Deferred local-CodeRabbit suggestion (explicit reviewer decision wanted): harden swapAndStartBridgeTokensViaPaxosTransit with a native-balance-delta check (require the post-swap native balance gain to cover nativeFee before _startBridge), so the LZ fee could never be paid from pre-existing stray diamond native balance. Not applied pre-push: it is a logic change to the already-reviewed fee-funding design (nativeFee may be funded by msg.value or by the ERC20→native pre-swap's reserved output; the Diamond holds no native between txs by invariant, and refundExcessNative only refunds balance gained within the call), so it should be a deliberate reviewer call rather than a silent pre-push edit.

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 5 commits June 30, 2026 13:51
…EXSC-547)

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

Addresses adversarial review:
- document destEID/wantAsset are trusted from the signed quote (AcrossV4 trust model)
- MockTransitStation now gates on msg.value (LayerZero fee) like the real station
- add tests: native-fee underpaid revert, non-zero fees, swap-output-below-offerAmount revert
- assert no funds stranded in / returned to the Diamond
- demo exercises the fee gate; honest success message

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…un.lock churn (EXSC-547)

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

coderabbitai Bot commented Jun 30, 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

Adds PaxosTransitFacet for forwarding Paxos-signed transit quotes to a TransitStation contract, with supporting interface, tests, deployment wiring, signing config, demo automation, and documentation.

Changes

Paxos Transit Facet

Layer / File(s) Summary
IPaxosTransit interface and PaxosTransitFacet core
src/Interfaces/IPaxosTransit.sol, src/Facets/PaxosTransitFacet.sol
Defines Route/Quote structs and submitOrder in IPaxosTransit. Implements PaxosTransitFacet with TRANSIT_STATION, LIFI_DISTRIBUTOR_CODE, PaxosTransitData, bridge/swap entry points, and quote validation plus order submission.
MockTransitStation and PaxosTransitFacet test suite
test/solidity/utils/MockTransitStation.sol, test/solidity/Facets/PaxosTransitFacet.t.sol
Adds MockTransitStation with fee gating, ERC20 pulling, and state recording. The Solidity tests cover fuzz bridging, revert paths, fee forwarding, custody behavior, fee handling, and swap refund behavior.
Deploy scripts, config, and deployment records
script/deploy/facets/DeployPaxosTransitFacet.s.sol, script/deploy/facets/UpdatePaxosTransitFacet.s.sol, script/deploy/resources/deployRequirements.json, config/paxosTransit.json, config/clearSigningProposal.json, deployments/mainnet.*.json, .agents/rules/102-facets.md
Adds deployment/update scripts, wires transitStation config loading, registers PaxosTransitFacet in deployment requirements, populates transit config, updates staging deployment manifests, adds clear-signing formats for the new entrypoints, and updates facet rules for refund routing and native-fee handling.
Demo script and documentation
script/demoScripts/demoPaxosTransit.ts, docs/PaxosTransitFacet.md
Adds a local demo script that deploys and exercises the full bridge flow, plus Markdown documentation for facet behavior, quote fields, validation rules, and sample requests.

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

Possibly related PRs

  • lifinance/contracts#1821: Adds the clear-signing formats for startBridgeTokensViaPaxosTransit and swapAndStartBridgeTokensViaPaxosTransit, which matches the new PaxosTransit entrypoints and signing shape.

Suggested labels: AuditRequired

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 names the new Paxos Transit bridge facet and related versioning, matching the main change.
Description check ✅ Passed The description covers the Linear task, rationale, completed checklist items, and reviewer notes, with only one non-critical facet checklist box left unchecked.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/exsc-547-paxos-transit-facet

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.

Comment thread src/Facets/PaxosTransitFacet.sol Dismissed
Comment thread src/Facets/PaxosTransitFacet.sol Dismissed
0xDEnYO and others added 2 commits June 30, 2026 14:48
…gainst live deployment (EXSC-547)

- config/paxosTransit.json: real station address (0x49AAA987...) on mainnet + outlaw
- pin IPaxosTransit.submitOrder selector to the deployed+verified contract (0x3784896a)
- docs: deployed addresses + verified funds-flow (msg.sender pull, public submitOrder) + Paxos-side route/signature deps

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

Deployed PaxosTransitFacet v1.0.0 to the mainnet STAGING diamond
(0xbEbCDb5093B47Cd7add8211E4c77B6826aF7bc5F) to validate the deploy
pipeline. Facet at 0x65cdFdA46FBfA8C584C441F396cd71972aa2520c, verified
on Etherscan; selectors startBridgeTokensViaPaxosTransit (0x8a911d00)
and swapAndStartBridgeTokensViaPaxosTransit (0xb33b5788) registered via
diamondCut.

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
script/demoScripts/demoPaxosTransit.ts (1)

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

Add an explicit return type to main.

waitForAnvil and deploy declare explicit return types; main should too for consistency.

Proposed change
-async function main() {
+async function main(): Promise<void> {
As per coding guidelines: "Use explicit return types for functions in TypeScript".
🤖 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/demoScripts/demoPaxosTransit.ts` at line 119, The main function in
demoPaxosTransit should have an explicit TypeScript return type for consistency
with waitForAnvil and deploy. Update the async main declaration to include its
return type, and verify any awaited calls inside main still align with that
signature.

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 `@docs/PaxosTransitFacet.md`:
- Around line 103-104: Fix the wording in the PaxosTransitFacet documentation
sentence so it reads naturally: hyphenate “swap-specific” and replace the
ungrammatical “to can be run” phrasing with a correct construction. Update the
prose in the affected doc section only, keeping the meaning the same and
preserving the references to the swap library, calldata array, and DEX examples.

---

Nitpick comments:
In `@script/demoScripts/demoPaxosTransit.ts`:
- Line 119: The main function in demoPaxosTransit should have an explicit
TypeScript return type for consistency with waitForAnvil and deploy. Update the
async main declaration to include its return type, and verify any awaited calls
inside main still align with that signature.
🪄 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: 8ba0e418-9aac-478a-9f24-61bb26c5c31f

📥 Commits

Reviewing files that changed from the base of the PR and between 6a2adce and 99289c2.

📒 Files selected for processing (12)
  • config/paxosTransit.json
  • deployments/mainnet.diamond.staging.json
  • deployments/mainnet.staging.json
  • docs/PaxosTransitFacet.md
  • script/demoScripts/demoPaxosTransit.ts
  • script/deploy/facets/DeployPaxosTransitFacet.s.sol
  • script/deploy/facets/UpdatePaxosTransitFacet.s.sol
  • script/deploy/resources/deployRequirements.json
  • src/Facets/PaxosTransitFacet.sol
  • src/Interfaces/IPaxosTransit.sol
  • test/solidity/Facets/PaxosTransitFacet.t.sol
  • test/solidity/utils/MockTransitStation.sol

Comment thread docs/PaxosTransitFacet.md Outdated
0xDEnYO and others added 3 commits June 30, 2026 17:07
…547)

Adds the startBridgeTokensViaPaxosTransit / swapAndStartBridgeTokensViaPaxosTransit
entries so the verify-clear-signing CI check passes.

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

The verify-clear-signing check builds the PR merge commit, so it sees both
main's LiFiIntentEscrowFacetV2 entries (#1964) and this branch's PaxosTransit
entries (105 total). Merge main and regenerate so the committed file matches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@melianessa melianessa added the requires-types Trigger Types Bindings CI (ABI/type generation for lifi-contract-types) label Jun 30, 2026

@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 (1)
config/clearSigningProposal.json (1)

1910-1912: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

nativeFee amount isn't surfaced in the clear-signing UI.

Both new formats hide all _paxosData fields, so the LayerZero nativeFee the user actually pays via msg.value is never shown in the confirmation screen — only _bridgeData.minAmount (ERC20) is displayed. This mirrors the existing convention for other native-fee-bearing facets (Stargate, Glacis, Hop) in this same file, so it's not a regression introduced by this PR, but worth considering for a future UX pass since it's real ETH the signer spends.
[optional_refactor]

Also applies to: 5172-5174

🤖 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 `@config/clearSigningProposal.json` around lines 1910 - 1912, The PaxosTransit
clear-signing entries currently hide all _paxosData fields, so the LayerZero
nativeFee paid via msg.value is not shown in the confirmation UI. Update the
clear-signing metadata for startBridgeTokensViaPaxosTransit to surface the
nativeFee alongside the existing _bridgeData.minAmount display, following the
same UI pattern used by other native-fee facets like Stargate, Glacis, and Hop.
Make the change in the relevant intent/interpolatedIntent definitions so users
can see the actual ETH cost before signing.
🤖 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 `@config/clearSigningProposal.json`:
- Around line 1910-1912: The PaxosTransit clear-signing entries currently hide
all _paxosData fields, so the LayerZero nativeFee paid via msg.value is not
shown in the confirmation UI. Update the clear-signing metadata for
startBridgeTokensViaPaxosTransit to surface the nativeFee alongside the existing
_bridgeData.minAmount display, following the same UI pattern used by other
native-fee facets like Stargate, Glacis, and Hop. Make the change in the
relevant intent/interpolatedIntent definitions so users can see the actual ETH
cost before signing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0705bc90-2dd5-487d-b0ba-315568ea635a

📥 Commits

Reviewing files that changed from the base of the PR and between 99289c2 and 0691276.

📒 Files selected for processing (2)
  • config/clearSigningProposal.json
  • script/deploy/resources/deployRequirements.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • script/deploy/resources/deployRequirements.json

0xDEnYO and others added 3 commits July 1, 2026 16:17
- docs: hyphenate 'swap-specific' and fix 'to can be run' grammar
- demo: add explicit Promise<void> return type to main()

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

- add refundRecipient to PaxosTransitData; positive slippage and swap
  leftovers now go there instead of msg.sender (which may be a relayer
  or the Permit2Proxy); zero-address check reverts InvalidCallData
- move the minAmount == offerAmount check from _startBridge into the
  non-swap entrypoint (the swap path assigns it, making the old check
  a tautology there)
- require nativeFee <= msg.value on both entrypoints so the LayerZero
  fee can never be paid from diamond balance
- pin the emitted LiFiTransferStarted minAmount to the quote's
  offerAmount in the positive-slippage test; add revert tests for the
  new guards (facet line/branch coverage: 100%)
- update docs, demo script, and clearSigningProposal tuple signatures
  for the new struct field

Co-Authored-By: Claude Fable 5 <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.

Actionable comments posted: 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.

Inline comments:
In `@src/Facets/PaxosTransitFacet.sol`:
- Around line 69-72: The swap flow in PaxosTransitFacet is using
PaxosData.quote.offerAmount as the effective output floor without rejecting
zero, so update the validation in the offerAmount/minAmount check and the
deposit-and-swap path to explicitly reject a zero offerAmount before it reaches
_depositAndSwap. Make sure the logic in the swap branch preserves the
bridge-side _bridgeData.minAmount as the enforced floor for source swaps, and
only allows the Paxos-signed offerAmount to match it when it is nonzero; apply
the same fix in the later swap handling referenced by the PaxosTransitFacet
flow.
🪄 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: eea615b5-7449-4da1-a25d-016370af21f0

📥 Commits

Reviewing files that changed from the base of the PR and between 0691276 and f2381f4.

📒 Files selected for processing (6)
  • config/clearSigningProposal.json
  • docs/PaxosTransitFacet.md
  • script/demoScripts/demoPaxosTransit.ts
  • script/deploy/resources/deployRequirements.json
  • src/Facets/PaxosTransitFacet.sol
  • test/solidity/Facets/PaxosTransitFacet.t.sol
✅ Files skipped from review due to trivial changes (1)
  • docs/PaxosTransitFacet.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • script/deploy/resources/deployRequirements.json
  • config/clearSigningProposal.json
  • script/demoScripts/demoPaxosTransit.ts
  • test/solidity/Facets/PaxosTransitFacet.t.sol

Comment thread src/Facets/PaxosTransitFacet.sol Outdated
0xDEnYO and others added 7 commits July 2, 2026 13:41
… pre-swap

The nativeFee <= msg.value guard broke the supported flow where the
LayerZero fee is paid from an ERC20->native pre-swap output (kept in
the diamond via _depositAndSwap's nativeReserve) with msg.value = 0.
Keep the guard on the non-swap path only, where msg.value is the sole
native source; add a test proving the pre-swap-funded flow end to end.

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

Lessons from the PaxosTransitFacet review (EXSC-547): user token value
(positive slippage + swap leftovers) goes to an explicit refundRecipient,
never msg.sender (relayer/Permit2Proxy); nativeFee <= msg.value may only
be enforced on non-swap entrypoints because swap paths can fund the fee
via an ERC20->native pre-swap kept by _depositAndSwap's nativeReserve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/quote, note >$5 min order (EXSC-547)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h new ABI

New facet 0x4DE53512F866ed4e5aa46806d63eB04C56FD04C3 registered via direct
diamondCut; stale selectors of the pre-refundRecipient build
(0x65cdFdA46FBfA8C584C441F396cd71972aa2520c) removed from the diamond and
the diamond log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… path (EXSC-547)

The swap entrypoint used quote.offerAmount as the swap floor without any
non-zero check - validateBridgeData only covered the incoming minAmount,
which was then overwritten. Enforcing minAmount == offerAmount on both
entrypoints extends the non-zero guarantee to the swap floor and makes
the overwrite redundant (removed). Flagged by CodeRabbit on PR #1996.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ransitFacet entries (EXSC-547)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h swap-path minAmount validation (EXSC-547)

New facet 0xFC28E13e221771E93a2Adb8E4e38a32fE44BdB93 (salt 02072026) registered
via direct diamondCut, replacing 0x4DE53512F866ed4e5aa46806d63eB04C56FD04C3
whose selectors were superseded on-chain; stale entry removed from the diamond
log. Same version 1.0.0 (pre-release churn, facet not yet in production).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread config/paxosTransit.json Outdated
Comment thread src/Facets/PaxosTransitFacet.sol Outdated
external
payable
nonReentrant
refundExcessNative(payable(msg.sender))

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.

Issue: LZ fee refund is misrouted for the Permit2Proxy path — on essentially every tx

TransitStation charges the actual LZ fee at inclusion and reverts if underpaid, while the on-chain fee drifts up with gas between quote-time and inclusion. So the backend must always send a native buffer, or txs revert. That means a nonzero refund is the normal, every-transaction outcome — not something that only happens on a backend mistake or a rare fee spike. The refund path is exercised on virtually every bridge.

Why that matters. Both facet entrypoints refund excess native via refundExcessNative(payable(msg.sender)):

  • Direct EOA / relayer call → fine, goes back to the payer.
  • Via Permit2Proxy → msg.sender is the proxy. The refund lands in Permit2Proxy (caught by its receive()) but is never forwarded — no sweep after the diamond call. It strands as owner-recoverable dust, not returned to whoever funded the buffer.

Combined with the point above: for the Permit2Proxy path this stranding happens every time, not occasionally. It's a steady leak, not a corner case.

Fix. Refund native to the backend-supplied refundRecipient instead of msg.sender, on both paths — as SupersetFacet (the closest analog: LZ-messaged, ERC20 offer, native = LZ fee) already does with a single refundAddress for excess native + swap leftovers + positive slippage. Strictly more expressive than msg.sender (backend picks the correct payee), and closes the leak without touching shared periphery.

Required with the change:

  1. Add a refundRecipient != address(0) guard to the non-swap path (only the swap path validates it today) — else excess native burns to address(0).
  2. Accept the minor footgun: a refundRecipient that rejects native reverts the bridge (self-inflicted; Superset accepts the same).
  3. Remove the now-inverted "Excess native stays with msg.sender — the caller funds the fee" comment in the swap path and the matching line in the PR description.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 005857d:

  • Both entrypoints now use refundExcessNative(payable(_paxosData.refundRecipient)) — nothing refunds to msg.sender anymore.
  • Added the refundRecipient != address(0) guard to the non-swap path (InvalidCallData, consistent with the swap path). One correction on the rationale there: a zero recipient wouldn't burn — LibAsset.transferNativeAsset reverts on address(0). The actual failure mode was a data-dependent late revert (tx success depending on whether LZ fee drift left an excess to refund); the guard makes it deterministic and early.
  • Fixed the inverted swap-path comment; struct NatSpec + docs/PaxosTransitFacet.md now document the routing and the accepted native-rejecting-recipient footgun (mirrors SupersetFacet.refundAddress).
  • Codified in [CONV:FACET-REFUNDS] (.agents/rules/102-facets.md); tests added: excess native → refundRecipient on both paths, zero refundRecipient reverts InvalidCallData on the non-swap path.

One factual nit on the mechanism: the TransitStation doesn't charge-and-refund itself — it forwards the whole msg.value to the LZ endpoint with refundAddress = msg.sender, so it's the endpoint that reverts on underpay / refunds the overage to the diamond mid-call. Same conclusion (a refund is the every-tx norm given the backend's buffer), different plumbing.

…h entrypoints (EXSC-547)

Excess source-side native (LZ fee overage the endpoint refunds to the
diamond mid-call) previously went back to msg.sender, stranding it in
Permit2Proxy as owner-recoverable dust on essentially every tx routed
through it. Route it to the explicit refundRecipient instead, mirroring
SupersetFacet's refundAddress.

Also adds the refundRecipient zero-check to the non-swap path: without
it a zero recipient reverts only late inside refundExcessNative
(LibAsset.transferNativeAsset reverts on address(0), it does not burn)
and only when LZ fee drift actually left an excess - a data-dependent
late revert. The guard makes it a deterministic early InvalidCallData,
consistent with the swap path.

Updates [CONV:FACET-REFUNDS], facet NatSpec, docs and tests accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…C-547)

The network was renamed outlaw -> robinhood (#2006, merged to main); the
deploy script looks up config/paxosTransit.json's transitStation address
by the live network key, so this branch's stale "outlaw" key would miss
on lookup post-rebase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@0xDEnYO 0xDEnYO marked this pull request as ready for review July 3, 2026 03:39
@lifi-action-bot

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/PaxosTransitFacet.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: False positive
🔹 Dismiss Comment: nonReentrant guards both entry points; [CONV:EVENTS] mandates LiFiTransferStarted at end of _startBridge, after the external call (same as GlacisFacet). Reordering would violate the convention.

🟢 View Alert - File: src/Facets/PaxosTransitFacet.sol
🔹 Calling a function without checking the return value may lead to silent failures. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/unused-return-function-call
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: submitOrder's returned bytes32 uuid is intentionally unused; transfers are tracked via bridgeData.transactionId. No clean Solidity discard exists (capturing to a var just trades for an unused-variable warning). Benign.

No unresolved security alerts! 🎉

@lifi-action-bot lifi-action-bot changed the title feat(PaxosTransitFacet): Paxos Transit bridge facet (EXSC-547) feat(PaxosTransitFacet): Paxos Transit bridge facet (EXSC-547) [PaxosTransitFacet v1.0.0, IPaxosTransit v1.0.0] Jul 3, 2026
…h refund-routing fix (EXSC-547)

New facet 0x3A5ef52B3aEEaA29e98C19806C187A8a774F5591 registered via direct
diamondCut, replacing 0x4DE53512F866ed4e5aa46806d63eB04C56FD04C3. That address
was itself stale: a prior redeploy attempt in this worktree read bytecode from
an un-rebuilt `out/` dir, silently re-deriving the CREATE3 salt of an even
older, pre-minAmount-validation build and landing back on an already-deployed,
superseded address instead of the current source. Caught by manually
re-deriving the salt against a clean `forge build` and comparing predicted
addresses. Both now-stale PaxosTransitFacet entries removed from the diamond
log. Same version 1.0.0 (pre-release churn, facet not yet in production).
@lifi-action-bot

Copy link
Copy Markdown
Collaborator

Test Coverage Report

Line Coverage: 90.09% (3420 / 3796 lines)
Function Coverage: 93.78% ( 528 / 563 functions)
Branch Coverage: 72.97% ( 632 / 866 branches)
Test coverage (90.09%) is above min threshold (89%). Check passed.

0xDEnYO and others added 4 commits July 3, 2026 11:20
…tic import (EXSC-547)

MockTransitStation.sol and TestToken.sol live under test/solidity/utils/,
which the validate-scripts CI job's typechain step never compiles (it only
runs `forge build src`). The demo's static `import ... from
'../../out/...json'` therefore fails TS2307 the moment that job actually
runs a type-check - previously masked because the job unconditionally
skips while the PR is in draft. Load the three artifacts via
fs.readFileSync + JSON.parse at runtime instead, sidestepping TS module
resolution entirely; a full local `forge build` still produces the files
for `bunx tsx` to read when the demo is actually run.

Verified: tsc-files --noEmit, script/utils/validateScripts.ts, and a live
run of the demo against a local anvil fork all pass.
… tests against the real TransitStation (EXSC-547)

Per [CONV:FORK-FIRST]: pin the fork to a block where Paxos has the
USDC->USDG route to Robinhood (EID 30416) fully configured, rotate the
station's quoteSigner to a test key via its real owner-gated setter, and
sign Quote structs in-test against the station's live EIP-712 domain
(new TestPaxosTransitBackendSig helper). Fund the LayerZero fee from the
station's own quoteSend.

New real-station coverage a mock could not provide: protocolFee/
integratorFee/net splits to the real Paxos recipients, replay guard
(SignatureAlreadyUsed + usedDigests), QuoteExpired, InvalidSigner,
LZ_InsufficientFee on underpaid fee, and endpoint fee-overage refunds
flowing back to the refundRecipient via refundExcessNative.

The demo script now runs the same flow against the real station on its
anvil mainnet fork instead of deploying a mock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… test mocks are acceptable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gate-lifting on the fork

Fork tests against mocks prove nothing a plain local node would not.
Expand the gate-lifting toolbox (owner prank on real setters, stdstore/
vm.store overrides, deal(), block re-pinning, protocol-quoted live
params), ban disguised mocking (vm.etch / vm.mockCall on the
counterparty), require setUp() guards on pinned config, and require
mock NatSpec + PR to document which techniques were ruled out.
Validated in practice by the PaxosTransitFacet fork-test rewrite
(EXSC-547).

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

lifi-qa-agent Bot commented Jul 3, 2026

Copy link
Copy Markdown

🔍 QA Review — EXSC-547

🔗 Linear Ticket · Pull Request #1996

⚠️ Re-review — one new commit since the prior QA review: merge from main (a854c24cbc42, 2026-07-03T08:23:32Z).

🧠 What this ticket does

PaxosTransitFacet — a bridge facet for Paxos Transit (ETH ↔ Robinhood Chain via a Paxos-signed, LayerZero-settled quote). Diamond pulls the offer asset, approves the TransitStation, calls submitOrder{value: nativeFee}. The facet delivers the full production implementation with 39 mainnet-fork tests against the real deployed station, deploy scripts, docs, and a runnable demo. The merge from main brought in an updated networks.json that now registers chain 4663 as "robinhood", resolving the config key mismatch flagged in the prior review.

Verdict: Needs Work — 2 of 7 prior items resolved by the main merge; 4 items remain open.


Issue Resolution Status

# Issue Status
#1 Author's "New Facet Contract Checklist" unchecked ❌ Still present
#2 config/paxosTransit.json / networks.json key mismatch (outlaw vs robinhood) Resolved — main merge brought updated networks.json; both files now use "robinhood"
#3 No zksync counterpart deploy script ❌ Still present
#4 PaxosTransitData.refundRecipient is last field; .agents/rules/102-facets.md requires receiverAddress first ❌ Still present
#5 CodeRabbit zero offerAmount inline thread has no author reply ❌ Still present
#6 Ticket AC absent — informational (assessment ticket vs. full facet delivery) ℹ️ Informational, no action required
#7 Duplicate of #2"robinhood" key was unrecognised Resolved with #2

Resolved by main merge

Issues #2 + #7 — Config key mismatch: ✅ Resolved

The merge commit (a854c24cbc42) brought networks.json from main, which now registers chain 4663 as "robinhood" (nativeCurrency: "ETH"). config/paxosTransit.json already used "robinhood". Both files now agree — no key lookup failure on deploy. Verified on the post-merge branch HEAD.


Outstanding items

#1 — [Medium] New Facet Contract Checklist checkbox still unchecked

The PR body still shows:

- [ ] For new facets: I have checked all points from this list: https://www.notion.so/lifi/New-Facet-Contract-Checklist-...

The merge did not change the PR body. This is a mandatory pre-merge gate for new facets. The author must work through the checklist and tick it — or provide an explicit acceptance comment documenting which items were reviewed and any that were intentionally deferred.


#3 — [Medium] No zksync counterpart deploy script

The PR file list contains zero zksync-pattern files. Per repo convention, new deployable contracts require a script/deploy/zksync/ counterpart. If Paxos Transit is not intended for zkSync networks (none of the configured chains in config/paxosTransit.json are zkSync chains), document this explicitly — either in the PR body, docs/PaxosTransitFacet.md, or script/deploy/resources/deployRequirements.json — so the absence reads as intentional.


#4 — [Medium] PaxosTransitData struct field ordering: receiverAddress must be first

.agents/rules/102-facets.md ([CONV:FACET-REQS]) states unambiguously: "receiverAddress first in {facetName}Data, must match bridgeData.receiver."

Current struct:

struct PaxosTransitData {
    IPaxosTransit.Quote quote;    // 1st
    bytes signature;              // 2nd
    uint256 nativeFee;            // 3rd
    address refundRecipient;      // 4th ← convention violation
}

The opaque-calldata exception in the rule governs the on-chain receiver validation requirement (EIP-712 signed quote binds receiver). It does not grant an exception to the field-ordering convention — these are two separate requirements. Reading the rule as written, receiverAddress must still be the first field regardless of where the on-chain receiver validation occurs.

Please either: (a) reorder so a receiverAddress-named field is first (even if it maps to the same address as refundRecipient), or (b) align with the SC team on whether a formal rule exception for opaque-calldata facets should be documented in .agents/rules/102-facets.md.


#5 — [Medium] CodeRabbit offerAmount inline thread — no author reply

The CodeRabbit comment at src/Facets/PaxosTransitFacet.sol (2026-07-02T06:33:56Z) flagging the zero offerAmount / zero swap-output-floor risk has received no author reply as of the post-merge HEAD. The code fix (minAmount == offerAmount enforcement on both paths) was made in commit 90ab11efccf1 before the prior review, so the fix may be in place — but the thread is unresolved.

Please reply to the thread pointing to the commit that resolved this, and mark the conversation as resolved.


🛡️ Version & Audit State

Contract Version Bumped in PR Audit log entry PR labels
PaxosTransitFacet v1.0.0 ✅ (new contract) ⏳ Not yet in audit/auditLog.json AuditRequired, requires-types

AuditRequired label correctly applied. Audit entry is pending — expected at this stage. PaxosTransitFacet must not be deployed to production before audit entry lands.


🧪 Test Coverage

Layer Score Files reviewed
Foundry unit/integration Good test/solidity/Facets/PaxosTransitFacet.t.sol (39 mainnet-fork tests against real TransitStation)
Deploy scripts Good DeployPaxosTransitFacet.s.sol, UpdatePaxosTransitFacet.s.sol present

CI coverage: 90.09% line, 93.78% function, 72.97% branch — above the 89% repo threshold.


🔗 Downstream Impact

Blocks: EXP-560 (parent Paxos Transit integration) — unblocks backend integration once merged and deployed.

Operational: AuditRequired label is correct; PaxosTransitFacet must not be deployed to production before at least a preliminary audit entry lands in audit/auditLog.json. The config key mismatch (#2) that would have blocked Robinhood Chain deployment is resolved by the main merge.


QA Agent — 2026-07-03

@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.

Requesting changes on 7 items — each requires either a code fix or an explicit acceptance comment with justification before this review is considered complete.

# Severity Type Issue / File
1 🟡 Medium Process Author's "New Facet Contract Checklist" left unchecked in PR author checklist
2 🟡 Medium Config config/paxosTransit.json key "robinhood"networks.json slug "outlaw" — deploy to chain 4663 fails
3 🟡 Medium Convention script/deploy/zksync/DeployPaxosTransitFacet.zksync.s.sol missing
4 🟡 Medium Convention PaxosTransitData.refundRecipient at position 4 — SC2 requires receiverAddress first
5 🟡 Medium Convention Unresolved CodeRabbit thread (zero offerAmount) — no author reply
6 🟡 Medium Process Ticket AC absent — EXSC-547 has no verifiable acceptance criteria
7 🟢 Low Convention config/paxosTransit.json "robinhood" key unrecognised — will confuse future contributors

1. [Medium] Author's "New Facet Contract Checklist" left unchecked

In the author's pre-review checklist, "For new facets: I have checked all points from this list" is explicitly unchecked (- [ ]). The checklist caption reads "DO NOT DEPLOY and contracts BEFORE CHECKING THIS". For a new bridge facet handling real user funds this is a mandatory pre-merge gate. Either complete the Notion checklist and tick the box, or provide a comment documenting which items were skipped and why.

2. [Medium] config/paxosTransit.json "robinhood" key does not match networks.json slug "outlaw" — hard deployment blocker

networks.json registers chain 4663 as "outlaw". DeployPaxosTransitFacet.s.sol resolves the constructor address with _getConfigContractAddress(path, ".transitStation." + network). Running forge script ... --network outlaw produces a null jq result → address(0) → constructor reverts InvalidConfig(). Rename the key to "outlaw" to match the authoritative slug, or open a separate PR renaming the chain in networks.json first.

3. [Medium] No script/deploy/zksync/DeployPaxosTransitFacet.zksync.s.sol

Every other deployed facet in this repo has a counterpart in script/deploy/zksync/. S7 convention requires the counterpart for every new deployable contract. Either add DeployPaxosTransitFacet.zksync.s.sol, or explicitly document in deployRequirements.json or the PR body that Paxos Transit is exempt from zkSync deployment.

4. [Medium] PaxosTransitData.refundRecipient at struct position 4 — SC2 requires receiverAddress first

.agents/rules/102-facets.md [CONV:FACET-REQS] mandates receiverAddress as the first field in {FacetName}Data. The refundRecipient field sits at position 4. The opaque-calldata exception satisfies the receiver-safety requirement but is separate from the field-ordering mandate. Either move refundRecipient to position 1, or confirm with the team that the exception waives ordering, document it in the facet NatSpec, and update the rules file. An explicit acceptance comment is required either way.

5. [Medium] Unresolved CodeRabbit inline thread — no author reply to zero offerAmount comment

CodeRabbit posted an inline comment on 2026-07-02 requesting a zero offerAmount guard before _depositAndSwap. There is no author reply and no fix commit. Even though the finding is a false positive (the validateBridgeData modifier covers this via minAmount == 0offerAmount == 0 tie), the thread must be explicitly closed with a reply explaining the existing protection. Unresolved /pr-ready threads are a signal the review was incomplete.

6. [Medium] Ticket AC absent — EXSC-547 has no verifiable acceptance criteria

The ticket was written as a feasibility assessment and carries no AC. The PR delivers a full production facet. Update the Linear ticket with 3-5 verifiable AC items so the QA sign-off has a concrete contract to verify against (e.g. successful submitOrder on-chain, funds routing, deploy readiness, audit coverage).

7. [Low] config/paxosTransit.json "robinhood" key is not a recognised network slug

Once the key is corrected to "outlaw" (item #2), the config entry will be valid. A _comment field explaining the purpose of the Robinhood Chain entry would prevent future confusion for contributors unfamiliar with the chain's current slug.

💡 Once you've addressed the items above, re-apply the "Agent Review Request" label to trigger an automated re-review.

@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.

Re-review after merge from main (a854c24, 2026-07-03T08:23:32Z). Issues #2 and #7 (config key mismatch) are now resolved by the merge — networks.json registers chain 4663 as "robinhood" and config/paxosTransit.json agrees. Four items remain open.

# Severity Type Issue / File
1 🟡 Medium Process "New Facet Contract Checklist" checkbox still unchecked in PR description
2 🟡 Medium Convention No zksync counterpart deploy script for PaxosTransitFacet
3 🟡 Medium Convention PaxosTransitData.refundRecipient is last field; .agents/rules/102-facets.md requires receiverAddress first
4 🟡 Medium Process CodeRabbit offerAmount inline thread has no author reply — unresolved thread

1. [Medium] New Facet Contract Checklist checkbox still unchecked

The PR body still has - [ ] For new facets: I have checked all points from this list: ... unchecked. This is a mandatory pre-merge gate for new facets. Please work through the checklist and tick it, or provide an explicit acceptance comment documenting any items intentionally deferred.

2. [Medium] No zksync counterpart deploy script

Zero zksync-pattern files appear in the PR file list. If Paxos Transit is not intended for zkSync chains, document this explicitly (e.g. a note in docs/PaxosTransitFacet.md or in script/deploy/resources/deployRequirements.json) so the absence reads as intentional rather than an oversight.

3. [Medium] PaxosTransitData struct — receiverAddress must be the first field

.agents/rules/102-facets.md ([CONV:FACET-REQS]) states: "receiverAddress first in {facetName}Data." The opaque-calldata exception covers the on-chain receiver-validation requirement — it does not waive the field-ordering convention. Current struct has refundRecipient last. Please either reorder so a receiverAddress-named field is first, or get SC team sign-off on a formal rule exception for opaque-calldata facets and document it in the rule file.

4. [Medium] CodeRabbit offerAmount inline thread — no author reply

The CodeRabbit comment at src/Facets/PaxosTransitFacet.sol (2026-07-02T06:33:56Z) has no author reply. The code fix (minAmount == offerAmount enforcement) was made in commit 90ab11efccf1. Please reply to the thread pointing to that commit and mark the conversation resolved.

💡 Once you've addressed the items above, re-apply the "Agent Review Request" label to trigger an automated re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

5 participants