-
Notifications
You must be signed in to change notification settings - Fork 137
L2 integration #1649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
L2 integration #1649
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1649 +/- ##
=======================================
Coverage 91.02% 91.02%
=======================================
Files 19 19
Lines 903 903
Branches 164 164
=======================================
Hits 822 822
Misses 64 64
Partials 17 17
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements L2 integration using the Across protocol to enable bridging between Ethereum L1, L2 chains (specifically Base Sepolia), and Polkadot AssetHub. It's a proof-of-concept (POC) demonstrating cross-chain token transfers including USDC and ETH/WETH.
Key Changes
- Added Solidity adaptor contracts (
SnowbridgeL1AdaptorandSnowbridgeL2Adaptor) to handle L1↔L2 token swaps via Across protocol - Implemented TypeScript API/SDK wrappers for L2 transfers in both directions (Polkadot→L2 and L2→Polkadot)
- Extended asset registry and environment configuration to support L2 chains with token swap routes
Reviewed changes
Copilot reviewed 47 out of 51 changed files in this pull request and generated 25 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/scripts/l2-integration/across/SnowbridgeL1Adaptor.sol | L1 adaptor contract for depositing tokens to L2 via Across |
| contracts/scripts/l2-integration/across/SnowbridgeL2Adaptor.sol | L2 adaptor contract for sending tokens to Polkadot via L1 gateway |
| web/packages/api/src/toEthereumSnowbridgeV2.ts | Added buildL2Call function for L2 transfer fee estimation and calldata generation |
| web/packages/api/src/toPolkadotSnowbridgeV2.ts | Added createL2TransferImplementation for L2→Polkadot transfers |
| web/packages/api/src/transfers/polkadotToL2/erc20ToL2.ts | Implementation for transferring ERC20 tokens from Polkadot to L2 |
| web/packages/api/src/transfers/l2ToPolkadot/erc20ToAH.ts | Implementation for transferring ERC20 tokens from L2 to Polkadot AssetHub |
| web/packages/api/src/across/api.ts | Wrapper for Across API fee estimation |
| web/packages/api/src/index.ts | Extended Context class with L2 adapter and configuration accessors |
| web/packages/api/src/environment.ts | Added L2 bridge configuration type and Across endpoints |
| web/packages/api/src/assets_v2.ts | Added findL2TokenAddress helper and registry build support for L2 chains |
| web/packages/operations/src/*.ts | Operation scripts for testing L2 transfers |
| web/packages/registry/src/westend_sepolia.registry.json | Registry updates with Base Sepolia chain and token mappings |
| contracts/scripts/l2-integration/across/test/*.s.sol | Test scripts for adaptor contracts |
Files not reviewed (1)
- web/pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| assets: { | ||
| wild: { | ||
| allCounted: 2, | ||
| allCounted: 3, |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The allCounted value is hardcoded to 3, but this change may need to be context-dependent. When transferring to L2, there might be different numbers of assets to deposit. This should be calculated dynamically based on the actual number of assets rather than hardcoded.
| amount: amount.toString(), | ||
| } | ||
| apiEndpoint += "/suggested-fees?" + new URLSearchParams(params) | ||
| let response = await fetch(apiEndpoint) |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HTTP response status should be checked before attempting to parse the JSON. If the API returns a non-200 status code, parsing the response as JSON might fail or return an error object instead of the expected fee data.
| let response = await fetch(apiEndpoint) | |
| let response = await fetch(apiEndpoint) | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch suggested fees: ${response.status} ${response.statusText}`) | |
| } |
| require(params.inputToken == address(0)); | ||
| require(params.inputAmount > params.outputAmount, "Input and output amount mismatch"); |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error messages in the require statements are not descriptive. Consider using more specific error messages that include information about what went wrong. For example, "Input token must be zero address" instead of just checking equality.
| require(params.inputToken == address(0)); | |
| require(params.inputAmount > params.outputAmount, "Input and output amount mismatch"); | |
| require(params.inputToken == address(0), "Input token must be zero address for native ETH deposits"); | |
| require(params.inputAmount > params.outputAmount, "Input amount must be greater than output amount"); |
| const ETHEREUM_ACCOUNT = new Wallet( | ||
| process.env.ETHEREUM_KEY ?? | ||
| "0x5e002a1af63fd31f1c25258f3082dc889762664cb8f218d86da85dff8b07b342", |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same hardcoded private key appears in multiple operation files across the codebase. This is a security concern even for test code. These hardcoded keys should be removed or the files should be clearly marked as test-only code with appropriate warnings.
| const ETHEREUM_ACCOUNT = new Wallet( | ||
| process.env.ETHEREUM_KEY ?? | ||
| "0x5e002a1af63fd31f1c25258f3082dc889762664cb8f218d86da85dff8b07b342", |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same hardcoded private key appears in multiple operation files across the codebase. This is a security concern even for test code. These hardcoded keys should be removed or the files should be clearly marked as test-only code with appropriate warnings.
| ) | ||
| } | ||
|
|
||
| let l2BridgeFeeInEther = 0n |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial value of l2BridgeFeeInEther is unused, since it is always overwritten.
| beneficiaryMultiAddress(beneficiaryAccount) | ||
|
|
||
| let assets: any = [], | ||
| value = 0n, |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial value of value is unused, since it is always overwritten.
|
|
||
| let assets: any = [], | ||
| value = 0n, | ||
| outputAmount = 0n |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial value of outputAmount is unused, since it is always overwritten.
| } else { | ||
| value = fee.totalFeeInWei | ||
| outputAmount = amount - fee.l2BridgeFeeInL2Token! | ||
| assets = [encodeNativeAsset(tokenAddress, outputAmount)] |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value assigned to assets here is unused.
| const l2TokenAddress = findL2TokenAddress(registry, l2ChainId, tokenAddress)! | ||
| const l1Adapter = context.l1Adapter() | ||
| let l1AdapterAddress = await l1Adapter.getAddress() | ||
| let l2BridgeFeeInL1Token = 0n |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial value of l2BridgeFeeInL1Token is unused, since it is always overwritten.
claravanstaden
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great @yrong! Have you thought about upgradeability for the adapter contracts? Or is it not a concern? I am thinking specifically about inflight transactions while upgrading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to audit these contracts?
| struct SwapParams { | ||
| address inputToken; | ||
| address outputToken; | ||
| uint256 inputAmount; | ||
| uint256 outputAmount; | ||
| uint256 destinationChainId; | ||
| } | ||
|
|
||
| struct SendParams { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we should move all the L2 contracts that need to be deployed to /src?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these files used for tests? Or just for reference?
| IGatewayV2 public immutable GATEWAY; | ||
| WETH9 public immutable L1_WETH9; | ||
| WETH9 public immutable L2_WETH9; | ||
| uint32 public TIME_BUFFER; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this value ever need to be changed?
| bytes32(uint256(uint160(params.inputToken))), | ||
| bytes32(uint256(uint160(params.outputToken))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should add a sanity check that inputToken > outputToken, same as depositNativeEther
require(params.inputAmount > params.outputAmount, "Input and output amount mismatch");
| registry: AssetRegistry, | ||
| tokenAddress: string, | ||
| ): TransferInterfaceToL2 { | ||
| // Todo: Support PNA transfers to L2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should perhaps throw an error if a PNA tokenAddress is provided?
| sourceDryRunError: any | ||
| assetHubDryRunError: any | ||
| bridgeHubDryRunError?: any | ||
| l2BridgeDryRunError?: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used anywhere?
Resolves: https://linear.app/snowfork/issue/SNO-1660
Context
This PR includes a POC demonstrating L2 integration using the Across protocol. The implementation is based primarily on the following documentation:
https://docs.across.to/reference/selected-contract-functions#deposit
https://docs.across.to/instant-bridging/embedded-crosschain-actions/crosschain-actions-integration-guide/using-the-generic-multicaller-handler-contract
https://docs.across.to/reference/api-reference#bridge-with-suggested-fees-api
For the GatewayV2 integration, we deployed an adaptor contract on Ethereum L1 as well as on each supported L2 (for example, Base in this POC). The adaptor contracts have already been deployed.
L1 Adaptor: https://sepolia.etherscan.io/address/0xf50FE3BA4306829a96A2d1E9d9518929E47753a6
L2 Adaptor: https://sepolia.basescan.org/address/0x5a7C107757ce0EDB72d65E4aa02e9cc40f5FcEC3
Wrap an API/SDK
Transfer USDC from L1 to L2
L2: https://sepolia.basescan.org/tx/0x1313889af68675450f8382ae59a7791087a0d8fbc73332bdf912c13eefbf13da
L1: https://sepolia.etherscan.io/tx/0xbf180c9a99905d11b6a6ec994d48f9412c623f9ee16072f3efdb6f6e651d049b
BH: https://bridgehub-westend.subscan.io/extrinsic/9805450-2
AH: https://assethub-westend.subscan.io/event/13382672-25
AH: https://assethub-westend.subscan.io/xcm_message/westend-7fa829ede3433924bcea7e37f5d21eef6584a5be
L1: https://sepolia.etherscan.io/tx/0x077c2b4e3fcbba2db421c867860c63961bcf5b56352caf105d19787b02c5f427
L2: https://sepolia.basescan.org/tx/0xa5a6fb920737e29771b3aea8e7028514f11dc6d056b6a836f228f2ede7faba9b
I prefer to start with limited support initially — transfer native Ether/WETH and stablecoins such as USDC and USDT. The adapter can be extended later to support arbitrary token swaps via a composable swap call on the source or the destination, if needed.