Skip to content

Transfer Assets from Ethereum into Polkadot

Introduction

This guide walks you through transferring tokens from Ethereum to Polkadot using the ParaSpell XCM SDK and Snowbridge. Snowbridge is a trustless, decentralized bridge integrated into the Polkadot protocol that enables secure asset transfers between the Ethereum and Polkadot ecosystems.

How the Bridge Works

The following diagram shows the complete flow when bridging WETH from Ethereum to Polkadot:

sequenceDiagram
    participant User
    participant Ethereum
    participant Relayers
    participant Polkadot

    User->>Ethereum: 1. Approve & send tokens
    Ethereum->>Ethereum: Lock tokens
    Note over Relayers: ~30 min relay
    Relayers->>Polkadot: 2. Relay message
    Polkadot->>Polkadot: Verify & mint tokens
    Polkadot-->>User: 3. Tokens available

In this guide, you will:

  • Set up an Ethereum development environment with the ParaSpell SDK
  • Approve tokens for the Snowbridge Gateway contract
  • Build and execute a bridge transfer from Ethereum to Polkadot Hub
  • Monitor the transfer status

Prerequisites

Before you begin, ensure you have the following:

  • A basic understanding of XCM.
  • Familiarity with JavaScript/TypeScript and Ethereum development.
  • Node.js v18 or higher and npm installed.
  • An Ethereum wallet with WETH tokens. See Prepare Tokens for Bridging for instructions to wrap ETH if needed.
  • A Polkadot account to receive the bridged assets.

Prepare Tokens for Bridging

To bridge ETH, you need to wrap it into WETH first. The WETH contract on Ethereum mainnet is:

0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

Why WETH instead of ETH?

Snowbridge only supports ERC-20 tokens—it cannot bridge native ETH directly. The bridge uses a standardized token interface to lock assets on Ethereum and mint corresponding representations on Polkadot. Since native ETH doesn't conform to the ERC-20 standard, you must first wrap it into WETH (Wrapped ETH), an ERC-20-compliant token pegged 1:1 to ETH.

You can wrap ETH by:

  1. Visiting the WETH contract on Etherscan
  2. Connecting your wallet
  3. Calling the deposit function with the amount of ETH you want to wrap

Initialize Your Project

Follow these steps to setup and initialize your project: 1. Create the project folder using the following commands:

mkdir eth-to-polkadot-bridge && \
cd eth-to-polkadot-bridge
  1. Initialize the JavaScript project:
npm init -y && npm pkg set type=module

Install dev dependencies:

npm install --save-dev @types/node tsx typescript

Install the ParaSpell SDK, its Snowbridge extension, and viem, which the extension uses to sign Ethereum transactions:

npm install @paraspell/sdk@14.4.4 @paraspell/evm-snowbridge@14.4.4 polkadot-api@3.3.0 viem@2.57.12

Now add the following setup code to index.ts:

index.ts
import { Builder, getBridgeStatus } from '@paraspell/sdk';
import { approveToken, getTokenBalance } from '@paraspell/evm-snowbridge';
import { createClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws';
import { createPublicClient, createWalletClient, formatUnits, http, type Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';

// Ethereum mainnet RPC endpoint (use your own API key)
const ETH_RPC = 'https://eth-mainnet.g.alchemy.com/v2/INSERT_API_KEY';

// Polkadot Bridge Hub RPC endpoint, used to check the bridge status
const BRIDGE_HUB_RPC = 'wss://polkadot-bridge-hub-rpc.polkadot.io';

// WETH contract address on Ethereum mainnet
const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';

// WETH has 18 decimals
const WETH_UNITS = 1_000_000_000_000_000_000n;

// Amount to bridge: 0.001 WETH (raw units as bigint)
const AMOUNT = WETH_UNITS / 1000n;

// Your Polkadot address to receive the bridged tokens (SS58 format)
const RECIPIENT_ADDRESS = 'INSERT_YOUR_POLKADOT_ADDRESS';

// Connect to Ethereum mainnet
// For a browser wallet (MetaMask), use custom(window.ethereum) as the transport
const walletClient = createWalletClient({
  account: privateKeyToAccount('INSERT_PRIVATE_KEY' as Hex),
  chain: mainnet,
  transport: http(ETH_RPC),
});
const publicClient = createPublicClient({ chain: mainnet, transport: http(ETH_RPC) });

Replace the following placeholders:

  • INSERT_API_KEY: Your Ethereum RPC provider API key.
  • INSERT_PRIVATE_KEY: The private key of the Ethereum account holding your WETH, as a 0x-prefixed hex string.
  • INSERT_YOUR_POLKADOT_ADDRESS: The Polkadot account address (SS58 format) that will receive the bridged tokens on Polkadot Hub.

Security Warning

Never commit private keys or seed phrases to production code. Use environment variables or secure key management systems.

Approve Tokens for Bridging

Before bridging ERC-20 tokens, you must approve the Snowbridge Gateway contract to spend your tokens. The @paraspell/evm-snowbridge package provides helper functions to check your balance and current allowance and to approve spending:

index.ts
async function approveTokens() {
  // Check current WETH balance and Snowbridge Gateway allowance
  const { balance, gatewayAllowance } = await getTokenBalance(walletClient, 'WETH');
  console.log(`Current WETH balance: ${formatUnits(balance, 18)}`);

  if (balance === 0n) {
    console.log('No WETH balance. Please wrap some ETH to WETH first.');
    console.log(`WETH contract: ${WETH_ADDRESS}`);
    process.exit(1);
  }

  if (gatewayAllowance >= AMOUNT) {
    console.log('Gateway allowance already covers the transfer amount.');
    return;
  }

  // Approve the Snowbridge Gateway contract to spend WETH
  const approveTxHash = await approveToken(walletClient, AMOUNT, 'WETH');
  console.log(`Approval transaction hash: ${approveTxHash}`);

  // Wait for confirmation
  await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
  console.log('Token approval confirmed!');
}

approveTokens();

Run the approval script:

npx tsx index.ts

You will see output confirming the token balance and approval similar to the following:

npx tsx index.ts Current WETH balance: 0.05 Approval transaction hash: 0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890 Token approval confirmed!

Build and Execute the Bridge Transfer

Now build and execute the bridge transfer from Ethereum to Polkadot Hub:

index.ts
async function bridgeToPolkadot() {
  console.log('Building bridge transaction...');
  console.log(`Bridging ${formatUnits(AMOUNT, 18)} WETH to Polkadot Hub`);
  console.log(`Recipient: ${RECIPIENT_ADDRESS}`);

  // Build and execute the bridge transfer
  // Note: 'AssetHubPolkadot' is the SDK identifier for Polkadot Hub
  const builder = Builder()
    .from('Ethereum')
    .to('AssetHubPolkadot')
    .currency({
      symbol: 'WETH',
      amount: AMOUNT,
    })
    .recipient(RECIPIENT_ADDRESS)
    .sender(walletClient);

  // Resolves once the Ethereum transaction is confirmed
  const txHash = await builder.signAndSubmit();
  await builder.disconnect();

  console.log('Bridge transaction confirmed on Ethereum!');
  console.log(`Transaction hash: ${txHash}`);
  console.log('Transfer will arrive in approximately 30 minutes.');
}

bridgeToPolkadot();

Comment out the approveTokens() call and run the transfer:

npx tsx index.ts

The signAndSubmit() call resolves once the transaction is confirmed on Ethereum and has emitted the bridge message. You will see the Ethereum transaction hash in the terminal output similar to the following:

npx tsx index.ts Building bridge transaction... Bridging 0.001 WETH to Polkadot Hub Recipient: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY Bridge transaction confirmed on Ethereum! Transaction hash: 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 Transfer will arrive in approximately 30 minutes.

Monitor the Transfer

Bridge transfers from Ethereum to Polkadot take approximately 30 minutes. You can monitor the transfer status in several ways:

You can also check the bridge status programmatically. First, comment out the bridgeToPolkadot() call so the script doesn't send another transfer. Then add the following code to index.ts:

index.ts
async function checkBridgeStatus() {
  // Check the current status of the Snowbridge
  const client = createClient(getWsProvider(BRIDGE_HUB_RPC));
  const status = await getBridgeStatus(client);
  console.log('Bridge Status:', status);
  client.destroy();
}

checkBridgeStatus();

Run the script:

npx tsx index.ts

The script prints Bridge Status: Normal while the bridge is operating.

Supported Assets and Destinations

Snowbridge can bridge any ERC-20 token from Ethereum, but the destination chain must have that asset registered as a foreign asset to receive it.

Verify Foreign Assets

Foreign asset creation on Polkadot Hub is permissionless—anyone can register a foreign asset. Before bridging, verify that the foreign asset registered on Polkadot Hub corresponds to the legitimate token on Ethereum by checking the asset's multilocation and confirming the Ethereum contract address matches the official token contract.

You can query which assets are supported for a specific route using the ParaSpell SDK as follows:

index.ts
import { getSupportedAssets } from '@paraspell/sdk';

// Get assets supported for Ethereum → Polkadot Hub transfers
const supportedAssets = getSupportedAssets('Ethereum', 'AssetHubPolkadot');
console.log('Supported assets:', supportedAssets.map(a => a.symbol));

Running this script will return a list of supported assets in the terminal similar to the following:

npx tsx get-supported-assets.ts Supported assets: [ 'sUSDe', 'WETH', 'PEPE', 'LDO', 'sUSDS', 'LBTC', 'EURC', 'SKY', 'USDC', 'MYTH', 'CGT2.0', 'TRAC', 'tBTC', 'wstETH', 'TONCOIN', 'DAI', 'SHIB', 'XRT', 'WBTC', 'TBTC', 'ETH', 'KILT', 'USDT', 'LINK', 'AAVE' ]

Common supported tokens on Polkadot Hub include:

Token Symbol Description
Wrapped Ether WETH Ethereum's native token wrapped as ERC-20
Wrapped Bitcoin WBTC Bitcoin wrapped as an ERC-20 token
Shiba Inu SHIB Popular ERC-20 meme token

To transfer to a different destination, change the .to() parameter and verify the asset is supported using the following code:

index.ts
// Check if WETH is supported on Hydration
const hydrationAssets = getSupportedAssets('Ethereum', 'Hydration');
const wethSupported = hydrationAssets.some(a => a.symbol === 'WETH');
console.log('WETH supported on Hydration:', wethSupported);

if (wethSupported) {
  const txHash = await Builder()
    .from('Ethereum')
    .to('Hydration')
    .currency({ symbol: 'WETH', amount: AMOUNT })
    .recipient(RECIPIENT_ADDRESS)
    .sender(walletClient)
    .signAndSubmit();
  console.log(`Transaction hash: ${txHash}`);
}
npx tsx bridge-to-hydration.ts WETH supported on Hydration: true Transaction hash: 0x1234...abcd
Complete index.ts script

Warning

The complete script calls approveTokens(), bridgeToPolkadot(), and checkBridgeStatus(). Every run of bridgeToPolkadot() sends another WETH transfer, so leave only the call for the step you want to run uncommented.

index.ts
import { Builder, getBridgeStatus } from '@paraspell/sdk';
import { approveToken, getTokenBalance } from '@paraspell/evm-snowbridge';
import { createClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws';
import { createPublicClient, createWalletClient, formatUnits, http, type Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';

// Ethereum mainnet RPC endpoint (use your own API key)
const ETH_RPC = 'https://eth-mainnet.g.alchemy.com/v2/INSERT_API_KEY';

// Polkadot Bridge Hub RPC endpoint, used to check the bridge status
const BRIDGE_HUB_RPC = 'wss://polkadot-bridge-hub-rpc.polkadot.io';

// WETH contract address on Ethereum mainnet
const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';

// WETH has 18 decimals
const WETH_UNITS = 1_000_000_000_000_000_000n;

// Amount to bridge: 0.001 WETH (raw units as bigint)
const AMOUNT = WETH_UNITS / 1000n;

// Your Polkadot address to receive the bridged tokens (SS58 format)
const RECIPIENT_ADDRESS = 'INSERT_YOUR_POLKADOT_ADDRESS';

// Connect to Ethereum mainnet
// For a browser wallet (MetaMask), use custom(window.ethereum) as the transport
const walletClient = createWalletClient({
  account: privateKeyToAccount('INSERT_PRIVATE_KEY' as Hex),
  chain: mainnet,
  transport: http(ETH_RPC),
});
const publicClient = createPublicClient({ chain: mainnet, transport: http(ETH_RPC) });

async function approveTokens() {
  // Check current WETH balance and Snowbridge Gateway allowance
  const { balance, gatewayAllowance } = await getTokenBalance(walletClient, 'WETH');
  console.log(`Current WETH balance: ${formatUnits(balance, 18)}`);

  if (balance === 0n) {
    console.log('No WETH balance. Please wrap some ETH to WETH first.');
    console.log(`WETH contract: ${WETH_ADDRESS}`);
    process.exit(1);
  }

  if (gatewayAllowance >= AMOUNT) {
    console.log('Gateway allowance already covers the transfer amount.');
    return;
  }

  // Approve the Snowbridge Gateway contract to spend WETH
  const approveTxHash = await approveToken(walletClient, AMOUNT, 'WETH');
  console.log(`Approval transaction hash: ${approveTxHash}`);

  // Wait for confirmation
  await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
  console.log('Token approval confirmed!');
}

approveTokens();

async function bridgeToPolkadot() {
  console.log('Building bridge transaction...');
  console.log(`Bridging ${formatUnits(AMOUNT, 18)} WETH to Polkadot Hub`);
  console.log(`Recipient: ${RECIPIENT_ADDRESS}`);

  // Build and execute the bridge transfer
  // Note: 'AssetHubPolkadot' is the SDK identifier for Polkadot Hub
  const builder = Builder()
    .from('Ethereum')
    .to('AssetHubPolkadot')
    .currency({
      symbol: 'WETH',
      amount: AMOUNT,
    })
    .recipient(RECIPIENT_ADDRESS)
    .sender(walletClient);

  // Resolves once the Ethereum transaction is confirmed
  const txHash = await builder.signAndSubmit();
  await builder.disconnect();

  console.log('Bridge transaction confirmed on Ethereum!');
  console.log(`Transaction hash: ${txHash}`);
  console.log('Transfer will arrive in approximately 30 minutes.');
}

bridgeToPolkadot();

async function checkBridgeStatus() {
  // Check the current status of the Snowbridge
  const client = createClient(getWsProvider(BRIDGE_HUB_RPC));
  const status = await getBridgeStatus(client);
  console.log('Bridge Status:', status);
  client.destroy();
}

checkBridgeStatus();

Transfer Assets Out of Polkadot

To transfer assets out of Polkadot, you can use the ParaSpell XCM SDK to build and execute a bridge transfer from Polkadot to Ethereum. Check out the Polkadot to Ethereum section of the ParaSpell XCM SDK documentation.

Where to Go Next

Last update: October 28, 2026
| Created: January 14, 2026