Skip to content

Integrate LayerSwap [LayerSwapFacet v1.0.0, ILayerSwapDepository v1.0.0]#1711

Open
gvladika wants to merge 36 commits into
mainfrom
add-layerswap-integration
Open

Integrate LayerSwap [LayerSwapFacet v1.0.0, ILayerSwapDepository v1.0.0]#1711
gvladika wants to merge 36 commits into
mainfrom
add-layerswap-integration

Conversation

@gvladika
Copy link
Copy Markdown
Contributor

@gvladika gvladika commented Apr 14, 2026

Which Linear task belongs to this PR?

https://linear.app/lifi-linear/issue/EXSC-172/sc-create-layerswapfacet

Why did I implement it this way?

Adds a new LayerSwapFacet that bridges tokens by depositing into the LayerSwap Depository contract on the source chain. The depository forwards funds to a LayerSwap-whitelisted receiver, and LayerSwap completes the bridge on the destination chain off-chain using the requestId correlated with an order created via the LayerSwap API.

  • Uses depositNative / depositERC20 on the depository directly. The depository validates the receiver against its own whitelist, so there is no need for on-chain receiver whitelisting in the facet.
  • depositoryReceiver is supplied per call by the LI.FI backend (as part of LayerSwapData) so LayerSwap can rotate receivers without redeploying the facet.
  • Supports non-EVM destinations via nonEVMReceiver (e.g. Solana), following the existing pattern used elsewhere in the codebase.
  • Adds EIP-712 signature verification from an authorized backend signer to cryptographically bind requestId to the destination receiver and other security-critical BridgeData/LayerSwapData fields. Without this binding, a caller could fund a LayerSwap order created for a different destination. Same pattern as UnitFacet / NEARIntentsFacet; backendSigner is read from config/global.json.
  • Adds replay protection via diamond storage tracking consumed requestIds (RequestAlreadyProcessed).
  • Ships with full unit tests (including 8 signature/replay cases), docs, deploy script, config/layer-swap.json, deployRequirements.json entry, and a viem-based demo script.

Checklist before requesting a review

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>

gvladika and others added 15 commits April 9, 2026 16:26
… CEI pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevent fund loss from invalid request IDs by requiring the LiFi backend
to sign bridge parameters. The facet now verifies an EIP-712 signature
over transactionId, requestId, minAmount, receiver, destinationChainId,
sendingAssetId, and signatureExpiry before executing any bridge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the EOA-transfer flow (with EIP-712 attestation and on-chain replay
protection) with direct calls to the LayerSwap Depository's depositNative /
depositERC20. The depository validates the receiver against its whitelist
and off-chain infra correlates the requestId with the API-created order,
so on-chain signature verification and consumedIds tracking are no longer
needed.

- Add ILayerSwapDepository interface
- Rewrite LayerSwapFacet around the depository, keeping non-EVM support
- LayerSwapData = { requestId, depositoryReceiver, nonEVMReceiver };
  depositoryReceiver is supplied per call by the LI.FI backend
- Rewrite tests with a MockLayerSwapDepository enforcing the whitelist
- Simplify deploy script and config to a single per-network depository
  address; drop backendSigner
- Update docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite demoLayerSwap.ts to match the new facet ABI: fetch the swap
via POST /api/v2/swaps with use_depository=true and extract requestId
and depositoryReceiver from deposit_actions[0].encoded_args. Covers
native and USDC bridging from Arbitrum.

Populate config/layer-swap.json with the LayerSwap depository address
(returned by the API for both mainnet and arbitrum).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop the DESTINATIONS map and isEVM flag. Accept the LayerSwap network
name directly via --to and pass it through to the API. Only special-case
SOLANA_MAINNET for non-EVM receiver handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

Review Change Stack

Warning

Rate limit exceeded

@gvladika has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 39 minutes and 37 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cd5745df-e40a-4dde-be1c-f797da3985d9

📥 Commits

Reviewing files that changed from the base of the PR and between 441985b and 4bd3011.

📒 Files selected for processing (3)
  • deployments/arbitrum.staging.json
  • src/Facets/LayerSwapFacet.sol
  • test/solidity/Facets/LayerSwapFacet.t.sol

Walkthrough

This PR adds LayerSwap integration by introducing configuration files, deployment scripts, and deployment address registration. It establishes per-network depository addresses and environment-scoped backend signer selection, enabling automated deployment of LayerSwapFacet across chains via Forge deployment scripts.

Changes

LayerSwap Facet Deployment Configuration

Layer / File(s) Summary
LayerSwap depository configuration
config/layerswap.json
Per-network depository address mappings for LayerSwap protocol integration.
Facet deployment script and requirements
script/deploy/facets/DeployLayerSwapFacet.s.sol, script/deploy/resources/deployRequirements.json
DeployScript loads depository and backend signer from config/layerswap.json and config/global.json, selecting environment via fileSuffix; deployRequirements.json registers both constructor arguments with zero-address validation.
Staged deployment address registration
deployments/arbitrum.staging.json
Adds LayerSwapFacet facet address entry after deployment.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically identifies the main change: integration of LayerSwapFacet v1.0.0 and ILayerSwapDepository v1.0.0, matching the substantial new facet additions throughout the changeset.
Description check ✅ Passed The description provides comprehensive coverage of the PR objectives, implementation rationale, and security considerations, with all author-provided checklist items marked complete; however, the reviewer checklist items (audit verification, external call validation, privileged call validation) remain unchecked as expected for pre-review phase.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-layerswap-integration

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
test/solidity/Facets/LayerSwapFacet.t.sol (1)

178-191: Add blank line between vm.expectRevert and the reverted call.

Per coding guidelines, there should be a blank line between vm.expectRevert() and the function call that is expected to revert. This applies to multiple test functions in this file.

♻️ Proposed fix
     function testRevert_WhenUsingZeroDepository() public {
         vm.expectRevert(InvalidConfig.selector);
+
         new LayerSwapFacet(address(0));
     }

     function testRevert_WhenDepositoryReceiverIsZero() public {
         validLayerSwapData.depositoryReceiver = address(0);

         vm.startPrank(USER_SENDER);
         usdc.approve(_facetTestContractAddress, bridgeData.minAmount);

         vm.expectRevert(InvalidCallData.selector);
+
         initiateBridgeTxWithFacet(false);
         vm.stopPrank();
     }

Similar changes needed at lines 270-271, 411-412, 423-424, and 435-436.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/solidity/Facets/LayerSwapFacet.t.sol` around lines 178 - 191, Tests
violate coding style by not having a blank line between vm.expectRevert(...) and
the call that should revert; update each affected test (e.g.,
testRevert_WhenUsingZeroDepository, testRevert_WhenDepositoryReceiverIsZero and
the other tests noted around lines 270, 411, 423, 435) to insert a single blank
line between the vm.expectRevert(...) invocation and the subsequent reverted
call (such as new LayerSwapFacet(...) or initiateBridgeTxWithFacet(...)) so the
expectRevert is visually separated from the action.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/layer-swap.json`:
- Around line 1-8: This file uses per-network top-level keys but should use the
single-parameter shape: make "layerSwapDepository" the top-level key with
"mainnet" and "arbitrum" as nested properties (i.e.,
.layerSwapDepository.<network>), update any deploy lookup/keyInConfigFile usage
to read that path, and adjust the relevant deploy script and the contract entry
in deployRequirements.json to match the new structure; locate references by the
config key name "layerSwapDepository" and by deployRequirements.json to ensure
all consumers are updated.

In `@docs/LayerSwapFacet.md`:
- Around line 45-51: The implementation lacks an on-chain commitment binding
requestId to the destination receiver and opaque calldata; restore EIP-712 (or
equivalent) signature verification by requiring a backend-signed authorization
that covers requestId, _bridgeData.receiver, and the hash of _layerSwapData
(including _layerSwapData.nonEVMReceiver) before proceeding; add a verifier
utility (e.g., verifyLayerSwapSignature) that checks the EIP-712 signature
against an authorized signer whitelist and call it at the start of the LayerSwap
entrypoint (the function that processes _layerSwapData / depositoryReceiver) to
revert if verification fails.

In `@script/demoScripts/demoLayerSwap.ts`:
- Around line 59-60: The hardcoded Solana receiver constant (SOLANA_NETWORK and
DEFAULT_SOLANA_RECEIVER) must be removed and replaced with a dynamically derived
Solana address from the signer's keypair when `--to SOLANA_MAINNET` and no
`--receiver` is provided; locate references to DEFAULT_SOLANA_RECEIVER
(including the other occurrences around the blocks referenced) and instead
compute the receiver by deriving the signer's public key/base58 address (from
the signer's Keypair or wallet object used in this script) and use that value
wherever DEFAULT_SOLANA_RECEIVER was used (e.g., in the swap/bridge call and
argument defaulting logic), ensuring CLI parsing falls back to the derived
address rather than a hardcoded constant.
- Around line 138-140: destinationChainId currently defaults every non-Solana
destination to 1, which can mismatch args.to (e.g., BASE_MAINNET); update the
logic around destinationChainId (and the related isSolana check) to derive the
LI.FI chain id from args.to when present (use your existing mapping function or
add a map from args.to network name to LI.FI chain id) and only fall back to
Number(args.chainId) or throw/require an explicit --chainId when args.to cannot
be resolved; ensure references to destinationChainId, isSolana, args.to,
args.chainId and LIFI_CHAIN_ID_SOLANA are used so the code validates input and
never silently assumes chainId 1 for non-Solana destinations.
- Around line 109-118: The createLayerSwapSwap function currently calls
fetch(LAYERSWAP_API, ...) without a timeout; replace that call with
fetchWithTimeout from script/utils/fetchWithTimeout.ts, passing the same options
and a 10000 ms timeout (and propagate the returned AbortSignal if the helper
requires it), so the POST to LAYERSWAP_API is bounded to 10 seconds; also add
the necessary import for fetchWithTimeout at the top of the file and keep the
rest of the JSON body/headers logic unchanged.
- Around line 1-10: Rewrite the script to replace all ethers usage with viem:
remove imports of ethers and ERC20__factory/LayerSwapFacet__factory and instead
create viem ContractClients using the ABI for ILiFi/LayerSwapFacet and the viem
provider/signer pattern (follow demoAcrossV4.ts structure and
DEV_WALLET_ADDRESS/provider helpers from demoScriptHelpers.ts); replace any
ethers utils.decode/format calls with viem-compatible helpers. Replace raw
fetch() calls with fetchWithTimeout() from script/utils/fetchWithTimeout.ts.
Remove the hardcoded DEFAULT_SOLANA_RECEIVER constant and derive the Solana
address at runtime using deriveSolanaAddress() from
script/demoScripts/utils/demoScriptHelpers.ts. Add a top JSDoc block describing
the module purpose and usage. Enforce chain ID handling by requiring a --chainId
argument or explicitly validating the provided chainId against the destination
network instead of defaulting to 1. Ensure all references to
LayerSwapFacet/ILiFi use ABI-based viem contract interactions and update
function names accordingly.

---

Nitpick comments:
In `@test/solidity/Facets/LayerSwapFacet.t.sol`:
- Around line 178-191: Tests violate coding style by not having a blank line
between vm.expectRevert(...) and the call that should revert; update each
affected test (e.g., testRevert_WhenUsingZeroDepository,
testRevert_WhenDepositoryReceiverIsZero and the other tests noted around lines
270, 411, 423, 435) to insert a single blank line between the
vm.expectRevert(...) invocation and the subsequent reverted call (such as new
LayerSwapFacet(...) or initiateBridgeTxWithFacet(...)) so the expectRevert is
visually separated from the action.
🪄 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: 6136f1b1-084e-42bd-ae90-b1c06f42a54f

📥 Commits

Reviewing files that changed from the base of the PR and between 35d68a3 and 1d7bf99.

📒 Files selected for processing (10)
  • config/layer-swap.json
  • deployments/arbitrum.diamond.staging.json
  • deployments/arbitrum.staging.json
  • docs/LayerSwapFacet.md
  • script/demoScripts/demoLayerSwap.ts
  • script/deploy/facets/DeployLayerSwapFacet.s.sol
  • script/deploy/facets/UpdateLayerSwapFacet.s.sol
  • src/Facets/LayerSwapFacet.sol
  • src/Interfaces/ILayerSwapDepository.sol
  • test/solidity/Facets/LayerSwapFacet.t.sol

Comment thread config/layerswap.json
Comment thread docs/LayerSwapFacet.md Outdated
Comment thread script/demoScripts/demoLayerSwap.ts Outdated
Comment thread script/demoScripts/demoLayerSwap.ts Outdated
Comment thread script/demoScripts/demoLayerSwap.ts
Comment thread script/demoScripts/demoLayerSwap.ts Outdated
gvladika and others added 11 commits April 14, 2026 16:20
Replace ethers with viem using shared helpers (setupEnvironment,
createContractObject, ensureBalance, ensureAllowance, executeTransaction).
Use fetchWithTimeout instead of raw fetch, derive Solana receiver
dynamically via deriveSolanaAddress, and resolve destination chain IDs
from a network-name lookup map instead of silently defaulting to 1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The facet lacked an on-chain binding between requestId and the
destination receiver, allowing a caller to fund a LayerSwap order
created for a different destination. Add backend EIP-712 signature
verification (same pattern as UnitFacet) to cryptographically commit
requestId, receiver, nonEVMReceiver, and all other security-critical
BridgeData/LayerSwapData fields before allowing the deposit.

Changes:
- LayerSwapFacet: add BACKEND_SIGNER immutable, LAYERSWAP_PAYLOAD_TYPEHASH,
  _verifySignature / _domainSeparator, diamond storage for requestId
  replay protection, extend LayerSwapData with signature + deadline
- DeployLayerSwapFacet: read backendSigner from staging/production config
- config/layer-swap.json: add staging/production backendSigner placeholders
- Tests: full EIP-712 test infrastructure, 8 new security test cases
  (invalid sig, expired sig, replay, cross-function replay, wrong
  receiver/requestId/nonEVMReceiver)
- Docs: updated struct, added signature verification section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Read PRIVATE_KEY_BACKEND_SIGNER_STAGING from env, create a signer
account, and produce the EIP-712 LayerSwapPayload signature before
calling startBridgeTokensViaLayerSwap. The LayerSwapData struct now
includes the required signature and deadline fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The backendSigner is shared across facets, so read it from
config/global.json instead of duplicating per-facet entries in
config/layer-swap.json, matching DeployNEARIntentsFacet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move layerSwapDepository to the top-level key with networks nested
beneath, matching the repo convention for single-parameter configs
(e.g. gaszip.json, garden.json). Update the deploy script's json path
and register LayerSwapFacet in deployRequirements.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/Facets/LayerSwapFacet.sol Dismissed
@gvladika gvladika changed the title Add LayerSwap facet Integrate LayerSwap Apr 15, 2026
@gvladika gvladika marked this pull request as ready for review April 15, 2026 12:02
@lifi-action-bot
Copy link
Copy Markdown
Collaborator

lifi-action-bot commented Apr 15, 2026

🤖 GitHub Action: Security Alerts Review 🔍

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

🟢 View Alert - File: src/Facets/LayerSwapFacet.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: Facet's entrypoint functions are not reentrant

No unresolved security alerts! 🎉

@lifi-action-bot lifi-action-bot marked this pull request as draft April 15, 2026 12:02
@lifi-action-bot lifi-action-bot changed the title Integrate LayerSwap Integrate LayerSwap [LayerSwapFacet v1.0.0, ILayerSwapDepository v1.0.0] Apr 15, 2026
Comment thread test/solidity/Facets/LayerSwapFacet.t.sol
@lifi-action-bot
Copy link
Copy Markdown
Collaborator

lifi-action-bot commented Apr 15, 2026

Test Coverage Report

Line Coverage: 88.20% (3254 / 3689 lines)
Function Coverage: 90.67% ( 496 / 547 functions)
Branch Coverage: 71.49% ( 602 / 842 branches)
Test coverage (88.20%) is above min threshold (83%). Check passed.

@gvladika gvladika marked this pull request as ready for review April 15, 2026 12:39
@lifi-action-bot lifi-action-bot marked this pull request as draft April 15, 2026 12:40
@gvladika gvladika marked this pull request as ready for review April 15, 2026 12:42
Comment thread config/layerswap.json
Comment thread src/Facets/LayerSwapFacet.sol Outdated
Comment thread src/Facets/LayerSwapFacet.sol
Comment thread src/Facets/LayerSwapFacet.sol
Comment thread src/Facets/LayerSwapFacet.sol
Comment thread test/solidity/Facets/LayerSwapFacet.t.sol Outdated
Comment thread src/Facets/LayerSwapFacet.sol Outdated
gvladika and others added 8 commits May 15, 2026 10:36
…tion

# Conflicts:
#	deployments/arbitrum.diamond.staging.json
Rename config/layer-swap.json to config/layerswap.json and put mainnet
first per .agents/rules/004-config-structure.md. Update the deploy
script path and the deployRequirements.json entry to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
.solhint.json sets immutable-vars-naming with immutablesAsConstants:
true, so the CONSTANT_CASE name already complies with the rule. Solhint
reports no error without the disable comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the MockLayerSwapDepository fixture and exercise the actual
LayerswapDepository deployed at 0xE226E4825CB215aBaFAd98fdd400583eAb6a594f
on mainnet (deployed 2026-03-26). Fork at block 25060964 and whitelist
the test depository receiver via an owner prank. The two failure-path
tests now pause the real depository (Pausable.EnforcedPause) and remove
the receiver from the whitelist (NotWhitelisted), respectively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Leftover from an earlier merge conflict resolution. validate-json CI
now passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the owner-prank whitelist setup and read an existing whitelisted
address (the Layerswap 1 EOA, 0x2Fc617...) from the on-chain whitelist
in setUp(). assertBalanceChange only asserts the delta inside the test
scope, so the receiver's real on-chain balance is irrelevant.
testRevert_WhenReceiverNotWhitelisted now signs for a deliberately
non-whitelisted address instead of mutating depository state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the nonEVMReceiver != bytes32(0) revert into the existing
`if (_bridgeData.receiver == NON_EVM_ADDRESS)` branch in _startBridge,
collapsing two NON_EVM_ADDRESS comparisons into one. _validateLayerSwapData
no longer needs _bridgeData, so its signature drops that parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gvladika added a commit that referenced this pull request May 29, 2026
Adds a thin Diamond facet that bridges stablecoins via Superset's hub-and-spoke
virtual pools (hub on Arbitrum; spokes on Base/Unichain). Mirrors the
LayerSwapFacet PR (#1711) layout: facet + interface + tests + deploy/update
scripts + config + docs + demo + deployRequirements entry.

Notes:
- Built against Superset's `develop` branch ABI (recipient/refundAddress/
  fallbackEoA arguments on `multiHopSwapWithOutputChain`). Not yet on mainnet;
  do not deploy until Superset ships the redeploy.
- ERC-20 + native ETH (facet wraps to WETH on source).
- Positive-slippage handled via SupersetData.amountOutMinPercent per
  [CONV:FACET-REQS].
- Non-EVM destinations rejected (Superset has no non-EVM spokes).
- 24/24 unit tests passing against a mock spoke pool manager.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants