китаев.tech

Bridge ETH на Base через OP Stack

Как перевести ETH из Ethereum Sepolia в Base Sepolia через L1StandardBridge OP Stack и проверить поступление на L2.

Что делает bridge ETH в OP Stack

Base построена на OP Stack, поэтому ETH переводится через стандартный L1→L2 bridge. На L1 вы отправляете ETH в L1StandardBridge, а на L2 тот же объём появляется на адресе получателя.

В примере используется Ethereum Sepolia → Base Sepolia. Это тестовые сети, но механика такая же, как при bridge в Base mainnet.


Создаём проект

bash
mkdir base-bridge-eth
cd base-bridge-eth

npm init -y
npm pkg set type="module"
npm install ethers dotenv
npm install --save-dev typescript tsx @types/node

mkdir src

Создайте файл .env:

bash
PRIVATE_KEY="0x..."
PARENT_RPC_URL="https://ethereum-sepolia-rpc.publicnode.com"
CHILD_RPC_URL="https://sepolia.base.org"
BRIDGE_AMOUNT_ETH="0.001"

PRIVATE_KEY должен принадлежать аккаунту с ETH в Ethereum Sepolia. Этот ETH оплатит L1 gas и будет переведён в Base Sepolia.


Отправляем ETH через L1StandardBridge

Создайте файл src/bridge-eth.ts:

typescript
import "dotenv/config";
import { setTimeout } from "node:timers/promises";
import { ethers } from "ethers";

const PARENT_CHAIN_ID = 11155111n;
const CHILD_CHAIN_ID = 84532n;

const PARENT_RPC_URL =
  process.env.PARENT_RPC_URL ?? "https://ethereum-sepolia-rpc.publicnode.com";
const CHILD_RPC_URL = process.env.CHILD_RPC_URL ?? "https://sepolia.base.org";

const privateKey = process.env.PRIVATE_KEY;
const amountEth = process.env.BRIDGE_AMOUNT_ETH ?? "0.001";

if (!privateKey) {
  throw new Error("Добавьте PRIVATE_KEY в .env");
}

const BASE_SEPOLIA_L1_STANDARD_BRIDGE =
  "0xfd0Bf71F60660E2f608ed56e1659C450eB113120";

const L1_STANDARD_BRIDGE_ABI = [
  "function bridgeETHTo(address _to,uint32 _minGasLimit,bytes _extraData) external payable",
];

const parentProvider = new ethers.JsonRpcProvider(PARENT_RPC_URL);
const childProvider = new ethers.JsonRpcProvider(CHILD_RPC_URL);
const wallet = new ethers.Wallet(privateKey, parentProvider);

async function assertNetwork(
  provider: ethers.JsonRpcProvider,
  expectedChainId: bigint,
  name: string,
) {
  const network = await provider.getNetwork();

  if (network.chainId !== expectedChainId) {
    throw new Error(
      `${name}: ожидался chainId ${expectedChainId}, получен ${network.chainId}`,
    );
  }
}

await assertNetwork(parentProvider, PARENT_CHAIN_ID, "Ethereum Sepolia");
await assertNetwork(childProvider, CHILD_CHAIN_ID, "Base Sepolia");

const destinationAddress = await wallet.getAddress();
const amount = ethers.parseEther(amountEth);

const parentBalance = await parentProvider.getBalance(destinationAddress);
const childBalanceBefore = await childProvider.getBalance(destinationAddress);

if (parentBalance < amount) {
  throw new Error(
    `Недостаточно ETH в Ethereum Sepolia: ${ethers.formatEther(parentBalance)}`,
  );
}

console.log("Destination:", destinationAddress);
console.log("Bridge amount:", amountEth, "ETH");
console.log(
  "Base balance before:",
  ethers.formatEther(childBalanceBefore),
  "ETH",
);

const bridge = new ethers.Contract(
  BASE_SEPOLIA_L1_STANDARD_BRIDGE,
  L1_STANDARD_BRIDGE_ABI,
  wallet,
);

const minGasLimit = 200_000;
const extraData = "0x";

const tx = await bridge.bridgeETHTo(destinationAddress, minGasLimit, extraData, {
  value: amount,
});

console.log("L1 bridge tx:", tx.hash);

const receipt = await tx.wait();

if (!receipt) {
  throw new Error("Транзакция не вернула receipt");
}

console.log("L1 confirmed in block:", receipt.blockNumber);
console.log(`L1 explorer: https://sepolia.etherscan.io/tx/${tx.hash}`);

const targetBalance = childBalanceBefore + amount;

for (let attempt = 1; attempt <= 40; attempt++) {
  const childBalance = await childProvider.getBalance(destinationAddress);

  if (childBalance >= targetBalance) {
    console.log("Base balance after:", ethers.formatEther(childBalance), "ETH");
    console.log(
      `L2 explorer: https://sepolia-explorer.base.org/address/${destinationAddress}`,
    );
    process.exit(0);
  }

  console.log(`Ждём поступление на Base... попытка ${attempt}/40`);
  await setTimeout(30_000);
}

throw new Error("ETH не поступил в Base Sepolia за 20 минут");

Запустите:

bash
npx tsx src/bridge-eth.ts

Скрипт отправит L1-транзакцию в Ethereum Sepolia, дождётся подтверждения и будет проверять баланс адреса в Base Sepolia.


Как это работает

BASE_SEPOLIA_L1_STANDARD_BRIDGE — адрес L1 Standard Bridge для Base Sepolia в Ethereum Sepolia.

bridgeETHTo(destinationAddress, minGasLimit, extraData, { value }) — создаёт OP Stack deposit: ETH блокируется на L1, а затем зачисляется получателю на L2.

destinationAddress — адрес, который получит ETH в Base Sepolia. В примере используется тот же EOA, который отправляет L1-транзакцию.

minGasLimit — минимальный gas limit для L2-части сообщения. Для обычного ETH bridge достаточно небольшого значения, в примере используется запас 200_000.

extraData — произвольные байты для bridge-сообщения. Для простого перевода ETH передаём 0x.

targetBalance = childBalanceBefore + amount — проверка, что депозит дошёл до Base Sepolia.


Почему нужен L1 bridge-контракт

Обычный sendTransaction() переводит ETH только внутри одной сети. Если отправить ETH на адрес в Ethereum Sepolia, он не появится в Base Sepolia.

OP Stack bridge делает две операции:

ШагСетьЧто происходит
L1 depositEthereum SepoliaETH отправляется в L1StandardBridge
L2 mintBase SepoliaBase зачисляет ETH на адрес получателя

Поэтому bridge-транзакция начинается на L1, а результат проверяется на L2.


Base Sepolia vs Ethereum Sepolia

СетьChain IDRPC в примереГде нужен ETH
Ethereum Sepolia11155111https://ethereum-sepolia-rpc.publicnode.comДля bridge-транзакции и L1 gas
Base Sepolia84532https://sepolia.base.orgДля будущих транзакций в Base

После bridge ETH окажется в Base Sepolia. Его можно использовать для деплоя контрактов и вызовов функций в Base.


Частые ошибки

ETH есть в Base Sepolia, но нет в Ethereum Sepolia → bridge начинается на L1. Для запуска нужен ETH именно в Ethereum Sepolia.

RPC URL ведёт не в ту сеть → проверьте chainId. Скрипт падает, если получил не 11155111 или не 84532.

Баланс на Base не изменился сразу → OP Stack deposit обычно занимает минуты. Дождитесь L2-зачисления и проверьте explorer.

Использовали адрес bridge от mainnet → для Base Sepolia нужен L1StandardBridge 0xfd0Bf71F60660E2f608ed56e1659C450eB113120.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.