ChainHopper Protocol SDK
ChainHopper Protocol allows Uniswap v3, v4 and Aerodrome LP positions to migrate between supported chains with a single click.
Supports:
- One-click migration from any v3/v4/Aerodrome pool to any v3/v4/Aerodrome pool
- Smart bridging: Single or Dual Token migrations
- Native token support for v4
- Initialize new pools through the migration, if needed
- Live on: Mainnet, Unichain, Base, Arbitrum, Optimism
We are grateful to Uniswap Foundation for funding and support!
Why use ChainHopper?
To move an LP position to another chain, it takes 4-5 manual steps:
- Remove liquidity & collect fees
- Swap/bridge "other" token
- Bridge WETH [Wait for confirmations...]
- Swap back, if needed
- Mint new position
Plus: you need gas tokens on destination chain đŤ.
With ChainHopper, you can do all this with one transaction.
Use this Typescript SDK to quickly and integrate with ChainHopper protocol with a few lines of code.
Installation
bun install chainhopper-sdk viem
Quick Start
1. Setup the ChainHopperClient
import { ChainHopperClient } from 'chainhopper-sdk';
export const client = ChainHopperClient.create({
rpcUrls: {
1: Bun.env.MAINNET_RPC_URL,
10: Bun.env.OPTIMISM_RPC_URL,
130: Bun.env.UNICHAIN_RPC_URL,
8453: Bun.env.BASE_RPC_URL,
42161: Bun.env.ARBITRUM_RPC_URL,
},
});
2. Retrieve Migration Data
To initiate a migration, you must provide the SDK with a single source LP position (sourcePosition) to be migrated along with parameters for at least one migration. Each migration requested requires a destination (destination) specifying the desired migration output parameters, and a path specification (either pathFilter or exactPath) specifying information about how the migration should be executed. The SDK then returns all the relevant data for submitting a migration transaction based on the parameters provided.
If the parameters provided allow for multiple destinations and paths, the various migration options will be returned in migrations in descending order of the estimated destination position value. If a migration requested is not available it will be returned in unavailableMigrations on the response along with the reasons why it is not available. If a single migration with an ExactPath is requested and that combination of migration and path are not available, it will throw an exception.
You can use both requestMigration with PathFilter to look for all migration requests matching the pathFilter or requestExactMigration for ExactPath requests where all of the paths are completely specified. In other words, when exactPath is used only the exact paths will be considered. If a pathFilter is used, specifiying an optional value will constrain the options to use that value. If an optional parameter is not specified in a PathFilter, all options for that parameter will be searched with the exception of slippageInBps which, if omitted, will default to 100 basis points or 1%.
In addition to requestMigration and requestExactMigration, corresponding functions requestMigrations and requestExactMigrations are available for requesting multiple combinations of destination and path (migrations) at once.
export type ExactPath = {
bridgeType: BridgeType;
migrationMethod: MigrationMethod;
slippageInBps: number;
};
export type PathFilter = {
bridgeType?: BridgeType;
migrationMethod?: MigrationMethod;
slippageInBps?: number;
};
import {
RequestMigrationParams,
RequestExactMigrationParams,
RequestMigrationsParams,
RequestExactMigrationsParams,
Protocol,
chainConfigs,
} from 'chainhopper-sdk';
import { zeroAddress } from 'viem';
const migrationParams: RequestMigrationParams = {
sourcePosition: {
chainId: 8453,
protocol: Protocol.UniswapV3,
tokenId: 1806423n,
},
destination: {
chainId: 130,
token0: zeroAddress,
token1: chainConfigs[130].usdcAddress,
fee: 500,
tickSpacing: 10,
hooks: zeroAddress,
tickLower: -250000,
tickUpper: -150000,
},
pathFilter: { bridgeType: BridgeType.Across },
};
const migrationResponse = await client.requestMigration(migrationParams);
const exactMigrationParams: RequestExactMigrationParams = {
sourcePosition: {
chainId: 8453,
protocol: Protocol.UniswapV3,
tokenId: 1806423n,
},
destination: {
chainId: 130,
token0: zeroAddress,
token1: chainConfigs[130].usdcAddress,
fee: 500,
tickSpacing: 10,
hooks: zeroAddress,
tickLower: -250000,
tickUpper: -150000,
},
exactPath: {
bridgeType: BridgeType.Across,
migrationMethod: MigrationMethod.SingleToken,
slippageInBps: 50,
},
};
const exactMigrationResponse = await client.requestExactMigration(exactMigrationParams);
const exactMigrationsParams: RequestExactMigrationsParams = {
sourcePosition: {
chainId: 8453,
protocol: Protocol.UniswapV3,
tokenId: 1806423n,
},
migrations: [
{
destination: {
chainId: 130,
protocol: Protocol.UniswapV4,
token0: zeroAddress,
token1: chainConfigs[130].usdcAddress,
fee: 500,
tickSpacing: 10,
hooks: zeroAddress,
tickLower: -250000,
tickUpper: -150000,
},
exactPath: {
bridgeType: BridgeType.Across,
migrationMethod: MigrationMethod.SingleToken,
slippageInBps: 100,
},
},
{
destination: {
chainId: 130,
protocol: Protocol.UniswapV4,
token0: zeroAddress,
token1: chainConfigs[130].usdcAddress,
fee: 100,
tickSpacing: 1,
hooks: zeroAddress,
tickLower: -250000,
tickUpper: -150000,
},
exactPath: {
bridgeType: BridgeType.Across,
migrationMethod: MigrationMethod.SingleToken,
slippageInBps: 100,
},
},
],
};
const exactMigrationsResponse =
await client.requestExactMigrations(exactMigrationsParams);
const migrationParams: RequestMigrationsParams = {
sourcePosition: {
chainId: 8453,
protocol: Protocol.UniswapV3,
tokenId: 1806423n,
},
migrations: [
{
destination: {
chainId: 130,
protocol: Protocol.UniswapV4,
token0: zeroAddress,
token1: chainConfigs[130].usdcAddress,
fee: 500,
tickSpacing: 10,
hooks: zeroAddress,
tickLower: -250000,
tickUpper: -150000,
},
pathFilter: {
slippageInBps: 100,
},
},
{
destination: {
chainId: 130,
protocol: Protocol.UniswapV4,
token0: zeroAddress,
token1: chainConfigs[130].usdcAddress,
fee: 100,
tickSpacing: 1,
hooks: zeroAddress,
tickLower: -250000,
tickUpper: -150000,
},
pathFilter: {
slippageInBps: 100,
},
},
],
};
const exactMigrationsResponse =
await client.requestExactMigrations(exactMigrationsParams);
3. Execute the Migration
You can execute the migration by using the data returned from either the general or exact migration requests:
import { createWalletClient, simulateContract, writeContract } from 'viem';
import { base } from 'viem/chains';
import { config } from './config';
import { NFTSafeTransferFrom } from 'chainhopper-sdk';
export const walletClient = createWalletClient({
chain: base,
transport: custom(window.ethereum!),
});
const migration = migrationResponse.migrations[0];
console.log(migrationResponse.unavailableMigrations);
const { request } = await walletClient.simulateContract({
...migration.executionParams,
account: migrationResponse.sourcePosition.owner,
});
const result = await writeContract(config, request);
const { request: exactRequest } = await walletClient.simulateContract({
...exactMigrationResponse.migration.executionParams,
account: exactMigrationResponse.sourcePosition.owner,
});
const exactResult = await writeContract(config, exactRequest);
const migration = migrationsResponse.migrations[0][0];
const { request: selectedMigration } = await walletClient.simulateContract({
...migration.executionParams,
account: migrationsResponse.sourcePosition.owner,
});
const exactResult = await writeContract(config, selectedMigration);
Calling writeContract open up a wallet window for signing the transaction and initiating the migration process.
Advanced Options
Sender Fees
ChainHopper protocol is designed to charge a protocol fee (currently set to 0) for any completed migration. Additionally, integrators can specify their own fees (for completed migrations), which the protocol will split with a percentage going to the specified recipient.
const migrationParams: RequestMigrationParams = {
senderShareBps: 15,
senderFeeRecipient: '0x...',
};
Migration Methods: Single Token vs Dual Token
ChainHopper Protocol supports two migration methods:
- Single Token: Converts the entire position to WETH (or USDC), migrates it, and then swaps back to the other token at the destination.
- Dual Token: Moves both tokens independently, reconstructing the position at the destination.
For example, to request a Dual Token route use pathFilter or exactPath to speciy it:
const migrationParams: RequestMigrationParams = {
sourcePosition: {
},
destination: {
},
pathFilter: {
migrationMethod: MigrationMethod.DualToken,
},
};
Slippage Parameters
By default, the SDK allows for 1% slippage, divided evenly across the source and destination chains. You can adjust this by specifying the slippageInBps parameter:
const migrationParams: RequestMigrationParams = {
path: {
slippageInBps: 100,
},
};
Creating a New Pool
ChainHopper Protocol (the smart contracts) supports creating a pool if it doesn't exist already. If you want to do this, you can specify a sqrtPriceX96 to initialize the new pool and a new pool will be initialized with this price to support the migration:
const migrationParams: RequestMigrationParams = {
sourcePosition: {
},
destination: {
sqrtPriceX96: 736087614829673861315061733n,
},
pathFilter: {
},
};
FAQs
1. What chains are supported?
Currently, we support Ethereum, Optimism, Arbitrum, Base and Unichain. Aerodrome protocol is supported on Base only. Please get in touch if you want us to support additional chains.
2. Do you have plans to support additional bridges?
Currently, we only support Across. We are considering adding Wormhole and Native Interop. If you have a request, please let us know.
3. What types of pools or tokens are supported?
Besides Fee-on-transfer and rebasing tokens, we support all tokens.
For pools, as long as there is a bridgeable asset in a pool, we can support it. Though, we caution users when using any pool with hooks, as those can lead to unpredictable scenarios.
4. Is this protocol Audited?
Yes. You can find the audit reports in protocol repository: ChainHopper Protocol.
5. How long does a migration typically take?
Most migrations with reasonable slippage (~1%) finish within 10 seconds.
6. How do fees work?
You, as the interface, can specify a fee and a recipient address to share that fee with. The protocol takes a 0.1% fee and additionally takes a 15% cut of the interface fee. So, if you specified 0.15% as the interface fee, the user will pay 0.25% which will be split 0.1225% for protocol and 0.1275% for you.
7. What happens if migration fails midway?
If migration fails on the source chain, nothing happens. User still owns the LP token and can retry.
If migration fails on the destination chian, the bridged asset will be delivered to the user's wallet on destination chain. And we will not take any fees for a failed migration.
In extremely rare scenarios, it's possible that an Across relayer was unable to deliver the asset on destination chain. In those situations, the attempted bridged asset - ETH, WETH, USDC - will be returned to the user on source chain.
You can open an issue on this repo or reach us at chainhopper@melio.io.