Support L2 to Parachain Bridge & Across Fee Optimization#1797
Merged
Conversation
…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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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).
alistair-singh
approved these changes
Jun 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ERC20ToParachainto route tokens from L2 chains to non-Asset-Hub parachains (like Hydration).• Destination Fee Forwarding: Introduced
destinationExecutionFeeto the L2 contract adapters'SendParamsto 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