Integrate LayerSwap [LayerSwapFacet v1.0.0, ILayerSwapDepository v1.0.0]#1711
Integrate LayerSwap [LayerSwapFacet v1.0.0, ILayerSwapDepository v1.0.0]#1711gvladika wants to merge 36 commits into
Conversation
… 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>
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughThis 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. ChangesLayerSwap Facet Deployment Configuration
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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: 6
🧹 Nitpick comments (1)
test/solidity/Facets/LayerSwapFacet.t.sol (1)
178-191: Add blank line betweenvm.expectRevertand 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
📒 Files selected for processing (10)
config/layer-swap.jsondeployments/arbitrum.diamond.staging.jsondeployments/arbitrum.staging.jsondocs/LayerSwapFacet.mdscript/demoScripts/demoLayerSwap.tsscript/deploy/facets/DeployLayerSwapFacet.s.solscript/deploy/facets/UpdateLayerSwapFacet.s.solsrc/Facets/LayerSwapFacet.solsrc/Interfaces/ILayerSwapDepository.soltest/solidity/Facets/LayerSwapFacet.t.sol
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>
🤖 GitHub Action: Security Alerts Review 🔍🟢 Dismissed Security Alerts with Comments 🟢 View Alert - File: ✅ No unresolved security alerts! 🎉 |
Test Coverage ReportLine Coverage: 88.20% (3254 / 3689 lines) |
…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>
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.
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
LayerSwapFacetthat bridges tokens by depositing into the LayerSwapDepositorycontract 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 therequestIdcorrelated with an order created via the LayerSwap API.depositNative/depositERC20on 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.depositoryReceiveris supplied per call by the LI.FI backend (as part ofLayerSwapData) so LayerSwap can rotate receivers without redeploying the facet.nonEVMReceiver(e.g. Solana), following the existing pattern used elsewhere in the codebase.requestIdto the destination receiver and other security-criticalBridgeData/LayerSwapDatafields. Without this binding, a caller could fund a LayerSwap order created for a different destination. Same pattern asUnitFacet/NEARIntentsFacet;backendSigneris read fromconfig/global.json.requestIds (RequestAlreadyProcessed).config/layer-swap.json,deployRequirements.jsonentry, and a viem-based demo script.Checklist before requesting a review
Checklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)