Skip to content

Support L2 to Parachain Bridge & Across Fee Optimization#1797

Merged
yrong merged 12 commits into
mainfrom
ron/l2-bridge-to-parachains
Jun 26, 2026
Merged

Support L2 to Parachain Bridge & Across Fee Optimization#1797
yrong merged 12 commits into
mainfrom
ron/l2-bridge-to-parachains

Conversation

@yrong

@yrong yrong commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Context

This PR introduces end-to-end support for bridging ERC20 tokens and Ether from Ethereum Layer 2 chains (e.g., Base, Arbitrum, Optimism) directly to non-Asset-Hub Polkadot Parachains (e.g., Hydration). It also optimizes fee estimation with Across by implementing message-aware gas suggestions.

Key Changes

• L2 to Parachain Bridge Route: Added a new transfer module ERC20ToParachain to route tokens from L2 chains to non-Asset-Hub parachains (like Hydration).
• Destination Fee Forwarding: Introduced destinationExecutionFee to the L2 contract adapters' SendParams to pass execution Ether surplus down to the destination chain for XCM execution.
• Across Suggested Fee Optimization: Implemented off-chain reconstruction of the Across message payload to query the /suggested-fees API with complete gas requirements. Implemented Across instruction serialization in both @snowbridge/provider-ethers and @snowbridge/provider-viem for the message payload.

Tests

yrong added 7 commits June 9, 2026 14:31
…egistry updates required for L2 bridging, separating them from the Ethereum dry-run transaction simulation features.

    ### Key Changes

    1. **L2 Fee Estimation Fixes**
       - Implemented `padFeeByPercentage` on `snowbridgeDeliveryFeeDOT` inside `estimateFeesFromAssetHub` to guard against slippage/underestimation and align with
  `estimateFeesFromParachains` behavior.
       - Replaced the hardcoded `"ETH"` symbol with the correct dynamically queried L2 token symbol in both `estimateFeesFromParachains` and `estimateFeesFromAssetHub`
  breakdowns and summaries.
       - Safely check for `tokenAmount` in L2 transfers, throwing a clear error if L2 transfers are missing required token amounts.

    2. **L2 Parachain Bridge Routing**
       - Added a new `ERC20FromParachain` class in `web/packages/api/src/transfers/polkadotToL2/erc20FromParachain.ts` to implement routing logic for transfers starting from
  non-Asset Hub parachains.
       - Refactored `web/packages/api/src/index.ts` to dynamically resolve between `ERC20FromAH` and `ERC20FromParachain` depending on the source parachain ID.
       - Renamed `erc20ToL2.ts` to `erc20FromAH.ts` for clarity.

    3. **Registry and Route Additions**
       - Added assets (`apyUSD`, `sUSDat`) to Parachain `2034` (Hydration) in `web/packages/registry/scripts/buildRegistry.ts`.
       - Enabled L2 transfer routes for `sUSDat`, while explicitly excluding `apyUSD` from any `ethereum_l2` routes (Optimism, Base, Arbitrum).
       - Regenerated mainnet bridge information (`polkadot_mainnet_bridge_info.g.ts`).
… via SendParams.

  Support L2 -> non-Asset-Hub parachain routes with ERC20ToParachain and factory routing. Add destinationExecutionFee to SendParams so the adaptor forwards destination
  execution ether as gateway surplus, fix Across fee estimates by including the destination message
# Conflicts:
#	web/packages/api/src/index.ts
#	web/packages/api/src/transfers/polkadotToL2/erc20FromParachain.ts
#	web/packages/base-types/src/index.ts
#	web/packages/operations/package.json
#	web/packages/registry/scripts/buildRegistry.ts
#	web/packages/registry/src/polkadot_mainnet_bridge_info.g.ts
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 79.06%. Comparing base (74486b1) to head (c5465c1).

Files with missing lines Patch % Lines
...ntracts/src/l2-integration/SnowbridgeL2Adaptor.sol 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1797      +/-   ##
==========================================
+ Coverage   78.33%   79.06%   +0.73%     
==========================================
  Files          24       24              
  Lines         983      984       +1     
  Branches      186      186              
==========================================
+ Hits          770      778       +8     
+ Misses        190      183       -7     
  Partials       23       23              
Flag Coverage Δ
solidity 79.06% <66.66%> (+0.73%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yrong yrong marked this pull request as ready for review June 15, 2026 07:37
@yrong yrong requested a review from alistair-singh June 15, 2026 08:06
@yrong yrong requested a review from claravanstaden June 15, 2026 08:45
yrong added 4 commits June 24, 2026 00:17
The Asset Hub dry-run reconstructed the delivered XCM from
`transfer.computed.totalValue`, the L2 deposit input amount. On the
ETH/WETH fee-token path `tx()` folds the Across origin fee
(`bridgeFeeInL2Token`) into that value, but the Across relayer keeps that
spread as its margin (deposit `inputAmount - outputAmount`); it never
reaches the gateway. The gateway receives `totalOutputAmount`
(`outputAmount + _gatewayValue(sendParams)`), so the dry-run overfunded
the reconstructed payload by `bridgeFeeInL2Token` and could pass while the
real message arrives with less Ether for destination execution/refund.

Subtract `bridgeFeeInL2Token` on the ETH/WETH path only (the swap path
funds the Across fee via the deposit, not msg.value, so it is not in
totalValue there). Applied to both the L2->Parachain and L2->Asset Hub
routes, which share the accounting.
On the ETH/WETH fee-token send path, computed.totalInputAmount was set to
`amount + bridgeFeeInL2Token`, but the adaptor pulls the full
depositParams.inputAmount (= value = amount + bridgeFeeInL2Token +
totalFeeInWei): as msg.value for native ETH, or via WETH
`safeTransferFrom` on the fee-token path. So the static balance/allowance
checks (and any approval derived from totalInputAmount) under-counted by
totalFeeInWei on WETH sends.

Set totalInputAmount to `value` so the checks match what the adaptor
transfers. Native ETH was already covered by the
`totalValue + executionFee` balance check; the swap path is unaffected
(totalInputAmount already equals depositParams.inputAmount there). Applied
to both the L2->Parachain and L2->Asset Hub routes.

Defence in depth: validate() also runs estimateGas, which already dry-runs
the real tx and rejects an under-funded allowance — so this does not
prevent an otherwise-uncaught on-chain revert. It replaces a cryptic
"could not estimate gas" failure with a clear GatewaySpenderLimitReached
error, and protects consumers that gate approval on totalInputAmount
without running estimateGas.
Reorders the flow to fee -> tx -> approve -> validate so the script can
read the exact value the adaptor pulls (transfer.computed.totalValue)
before approving. Normal behaviour is unchanged.

Adds REPRO_TIGHT_ALLOWANCE mode: approves the adaptor for exactly the
amount the static validation checks against (value - totalFeeInWei),
landing the allowance inside the [validationChecks, value) window so the
WETH under-count is deterministic. Implies dry-run so no transfer is
submitted. Narrates the three outcomes: patched (rejected on the allowance
check), unpatched (static checks pass but estimateGas backstops the
revert), and inconclusive (rejected for an unrelated reason such as low
balance).
@yrong yrong merged commit 161ae16 into main Jun 26, 2026
3 checks passed
@yrong yrong deleted the ron/l2-bridge-to-parachains branch June 26, 2026 02:52
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.

2 participants