Skip to content

feat(MayanFacet): signed arbitrary destination-call payloads (EXSC-604)#2043

Closed
0xDEnYO wants to merge 6 commits into
feature/exsc-364-sc-mayanfacet-route-between-forwardeth-and-swapandforwardethfrom
feature/exsc-604-sc-mayanfacet-support-arbitrary-destination-call-payloads
Closed

feat(MayanFacet): signed arbitrary destination-call payloads (EXSC-604)#2043
0xDEnYO wants to merge 6 commits into
feature/exsc-364-sc-mayanfacet-route-between-forwardeth-and-swapandforwardethfrom
feature/exsc-604-sc-mayanfacet-support-arbitrary-destination-call-payloads

Conversation

@0xDEnYO

@0xDEnYO 0xDEnYO commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Which Linear task belongs to this PR?

https://linear.app/lifi-linear/issue/EXSC-604/sc-mayanfacet-support-arbitrary-destination-call-payloads-swift-v2

Stacked on #2042 (EXSC-364). Base branch is the EXSC-364 branch, not main. Please review/audit the two together — the whole point of the stack is one audit over the combined change. Merge #2042 first; this retargets to main automatically.

Why did I implement it this way?

MayanFacet forbade destination calls (doesNotContainDestinationCalls on both entrypoints). Mayan Swift v2 orders can carry a customPayload that makes the destination executor perform an arbitrary call, whose target/calldata cannot be validated on-chain against bridgeData.receiver. Per .agents/rules/102-facets.md (opaque calldata flows), the sanctioned mitigation is a backend EIP-712 signature committing to the BridgeData fields plus a hash of the opaque calldata — exactly the pattern already shipped in UnitFacet/NEARIntentsFacet, which this reuses verbatim.

Design:

  • hasDestinationCall == true → verify a backend signature at the top of each entrypoint (before any deposit/swap), over {transactionId, receiver (NON_EVM-aware), minAmount, sendingAssetId, destinationChainId, hasDestinationCall, mayanProtocol, keccak256(protocolData), deadline}. On this path the receiver is signer-attested; _parseReceiver is skipped. transactionId is consumed CEI (marked used before the external Mayan call) for replay protection; the domain separator binds block.chainid + address(this) (diamond, under delegatecall).
  • hasDestinationCall == falseunchanged, permissionless _parseReceiver path. No signature required; existing traffic is not gated.
  • MayanData appends bytes signature + uint256 deadline (after 364's four fields; 364 order untouched). Immutable BACKEND_SIGNER set in the constructor; rotation is a governed facet-redeploy + diamondCut (no owner setter). solady/utils/ECDSA (reverts on malformed sig — no silent address(0)).
  • _parseReceiver, _parseHypercoreReceiver, _normalizeAmount, _replaceInputAmount, and 364's native-swap branch are untouched.

Version: 2.0.0 → 3.0.0 (constructor-signature + ABI change — MAJOR).

Security review: an independent adversarial pass (forgery / replay / ECDSA / branch-integrity / coexistence / storage / test-quality lenses, cross-checked against the Unit/NEAR references) found no critical or high-severity issue. Two informational notes: (1) the native source-swap params (swapProtocol/swapData/middleToken/minMiddleAmount) are intentionally not in the signed struct — not theft-exploitable, since the destination call lives in the committed protocolData and only the submitter's own msg.value is at stake; (2) signature malleability is harmless because replay keys on transactionId (in the digest), not the signature bytes.

Testing: forge test --match-contract MayanFacet → 55 pass, incl. H1–H6 happy paths and N1–N10 negative guards (forged payload incl. customPayload-only tamper, absent/wrong signer, receiver/route mismatch, expired, same- and cross-chain replay, flag-flip both directions, malformed sig). Clean under the solc-0.8.17/london floor build (no stack-too-deep).

Backend signer

config/mayan.json.backendSigner reuses the shared LI.FI backend signer — the same key Unit/NEAR use (config/global.json) — per an explicit decision to reuse the existing signer rather than provision a dedicated one. No separate key setup is required before deploy. (Tests use a stub signer and are independent of the config value.)

Follow-ups (separate tickets, out of scope here)

  • Unsigned-customPayload smuggling hardening (design OQ-3).
  • refundExcessNative(msg.sender) → explicit refundRecipient (design OQ-4; pre-existing).

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 — bypassed per the sanctioned interim (rate limits); independent adversarial security review done instead, documented above
  • I have added tests that cover the functionality / test the bug
  • For new facets: I have checked all points from this list — N/A (existing facet)
  • 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 2 commits July 13, 2026 09:08
…(EXSC-604)

Stacked on EXSC-364. Relaxes doesNotContainDestinationCalls on both entrypoints
and gates hasDestinationCall==true behind a backend EIP-712 signature over the
BridgeData fields + keccak256(protocolData); the signed path skips _parseReceiver
(receiver becomes signer-attested), consumes transactionId (CEI) for replay
protection, and binds chainid + diamond via the domain separator. Plain bridges
(hasDestinationCall==false) stay permissionless and unchanged. Reuses the shipped
UnitFacet/NEARIntentsFacet pattern (solady ECDSA, immutable BACKEND_SIGNER,
governance-only rotation). Version 2.0.0 -> 3.0.0 (constructor + ABI change).

config/mayan.json backendSigner is intentionally the zero address so any deploy
fails the constructor zero-check until BE provisions a DEDICATED Mayan signer key
(must NOT reuse Unit's).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keep the demo literal in sync with the 9-field MayanData struct.

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

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dd866c8e-2c4f-4130-8bdb-f4d55da56b33

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/exsc-604-sc-mayanfacet-support-arbitrary-destination-call-payloads

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.

@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 marked this pull request as draft July 13, 2026 02:13
Comment thread src/Facets/MayanFacet.sol Dismissed
Comment thread src/Facets/MayanFacet.sol Dismissed
@lifi-action-bot

Copy link
Copy Markdown
Collaborator

Test Coverage Report

Line Coverage: 90.14% (3450 / 3827 lines)
Function Coverage: 93.81% ( 531 / 566 functions)
Branch Coverage: 73.17% ( 641 / 876 branches)
Test coverage (90.14%) is above min threshold (89%). Check passed.

0xDEnYO and others added 4 commits July 13, 2026 09:30
…-and-swapandforwardeth' into feature/exsc-604-sc-mayanfacet-support-arbitrary-destination-call-payloads
…ta (EXSC-604)

MayanData gained signature+deadline; regenerate config/clearSigningProposal.json.

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

Per decision to reuse the existing shared signer key rather than a dedicated one:
config/mayan.json.backendSigner now holds the shared value (matches config/global.json
and config/unit.json). Removes the earlier zero-address fail-safe placeholder; no
separate key provisioning is needed before deploy.

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

0xDEnYO commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Closing: we decided not to implement arbitrary destination-call support on Mayan Swift v2 for now (EXSC-604 → Canceled).

The signed-payload mechanism worked, but the blocker is destination-call failure handling: Mayan Swift has no LI.FI Receiver in the flow, so our usual 'deliver at least the bridged token to the user on the destination if the destination swap fails' guarantee does not apply. A failed/unfulfilled Swift order refunds to the source trader after the deadline, and what happens when the opaque customPayload destination call itself reverts is undocumented in Mayan's public docs and unverified. Not shipping an opaque arbitrary-call surface with unclear failure semantics.

EXSC-364 (#2042, ETH routing) is unaffected and stays open — this PR was only stacked on it. Can reopen if we revisit.

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

Labels

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.

3 participants