Skip to content

Conversation

@yrong
Copy link
Contributor

@yrong yrong commented Dec 9, 2025

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

contracts git:(ron/l2-integration-across) ✗ make test-snowbridge-l1-adaptor
  • Transfer USDC from L2 to AssetHub
operations git:(ron/l2-integration-across) ✗ pnpm transferUSDCFromL2ToAH

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

  • Transfer USDC from AssetHub back to L2
➜  operations git:(ron/l2-integration-across) ✗ pnpm transferUSDCFromAHToL2

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.

@yrong yrong mentioned this pull request Dec 9, 2025
@yrong yrong changed the title L2 integration Using Across L2 integration Dec 9, 2025
@codecov
Copy link

codecov bot commented Dec 11, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.02%. Comparing base (1cf39a2) to head (cd9172e).

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           
Flag Coverage Δ
solidity 91.02% <ø> (ø)

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

☔ View full report in Codecov by Sentry.
📢 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 January 6, 2026 03:31
Copy link

Copilot AI left a 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 (SnowbridgeL1Adaptor and SnowbridgeL2Adaptor) 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,
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
amount: amount.toString(),
}
apiEndpoint += "/suggested-fees?" + new URLSearchParams(params)
let response = await fetch(apiEndpoint)
Copy link

Copilot AI Jan 6, 2026

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.

Suggested change
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}`)
}

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +54
require(params.inputToken == address(0));
require(params.inputAmount > params.outputAmount, "Input and output amount mismatch");
Copy link

Copilot AI Jan 6, 2026

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.

Suggested change
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");

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +32
const ETHEREUM_ACCOUNT = new Wallet(
process.env.ETHEREUM_KEY ??
"0x5e002a1af63fd31f1c25258f3082dc889762664cb8f218d86da85dff8b07b342",
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +27
const ETHEREUM_ACCOUNT = new Wallet(
process.env.ETHEREUM_KEY ??
"0x5e002a1af63fd31f1c25258f3082dc889762664cb8f218d86da85dff8b07b342",
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
)
}

let l2BridgeFeeInEther = 0n
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
beneficiaryMultiAddress(beneficiaryAccount)

let assets: any = [],
value = 0n,
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.

let assets: any = [],
value = 0n,
outputAmount = 0n
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
} else {
value = fee.totalFeeInWei
outputAmount = amount - fee.l2BridgeFeeInL2Token!
assets = [encodeNativeAsset(tokenAddress, outputAmount)]
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
const l2TokenAddress = findL2TokenAddress(registry, l2ChainId, tokenAddress)!
const l1Adapter = context.l1Adapter()
let l1AdapterAddress = await l1Adapter.getAddress()
let l2BridgeFeeInL1Token = 0n
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@claravanstaden claravanstaden left a 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.

Copy link
Contributor

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?

Comment on lines +18 to +26
struct SwapParams {
address inputToken;
address outputToken;
uint256 inputAmount;
uint256 outputAmount;
uint256 destinationChainId;
}

struct SendParams {
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs docs.

Copy link
Contributor

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?

Copy link
Contributor

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;
Copy link
Contributor

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?

Comment on lines +38 to +39
bytes32(uint256(uint160(params.inputToken))),
bytes32(uint256(uint160(params.outputToken))),
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this used anywhere?

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.

3 participants