китаев.tech

EIP-1559 — maxFeePerGas vs gasPrice

Как работают комиссии EIP-1559 в ethers v6: base fee, priority fee, maxFeePerGas и отличие от legacy gasPrice.

Что изменил EIP-1559

EIP-1559 заменил фиксированный gasPrice на две настройки комиссии: maxFeePerGas и maxPriorityFeePerGas.

Теперь отправитель задаёт верхний предел цены gas и чаевые валидатору, а сеть сама рассчитывает обязательную base fee для каждого блока.


Отправляем EIP-1559 транзакцию

Запустите локальную сеть Hardhat в отдельном терминале:

bash
npx hardhat node

Затем выполните TypeScript-код:

typescript
import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");

// Первый тестовый аккаунт Hardhat. Не используйте этот ключ в mainnet.
const sender = new ethers.Wallet(
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
  provider,
);

// Второй тестовый аккаунт Hardhat.
const recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";

const latestBlock = await provider.getBlock("latest");

if (latestBlock?.baseFeePerGas == null) {
  throw new Error("Сеть не поддерживает EIP-1559");
}

const maxPriorityFeePerGas = ethers.parseUnits("2", "gwei");
const maxFeePerGas = latestBlock.baseFeePerGas * 2n + maxPriorityFeePerGas;

console.log("Base fee:", ethers.formatUnits(latestBlock.baseFeePerGas, "gwei"), "gwei");
console.log("Max priority fee:", ethers.formatUnits(maxPriorityFeePerGas, "gwei"), "gwei");
console.log("Max fee:", ethers.formatUnits(maxFeePerGas, "gwei"), "gwei");

const tx = await sender.sendTransaction({
  to: recipient,
  value: ethers.parseEther("0.1"),
  type: 2,
  maxFeePerGas,
  maxPriorityFeePerGas,
});

console.log("Transaction hash:", tx.hash);
console.log("Transaction type:", tx.type);
console.log("Gas limit:", tx.gasLimit.toString());

const receipt = await tx.wait();

if (!receipt || receipt.status !== 1) {
  throw new Error("Транзакция не была успешно выполнена");
}

const minedBlock = await provider.getBlock(receipt.blockNumber);

if (minedBlock?.baseFeePerGas == null) {
  throw new Error("Блок не содержит baseFeePerGas");
}

const effectiveGasPrice = receipt.gasPrice;
const actualPriorityFee = effectiveGasPrice > minedBlock.baseFeePerGas
  ? effectiveGasPrice - minedBlock.baseFeePerGas
  : 0n;
const actualFee = receipt.gasUsed * effectiveGasPrice;

console.log("Block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Mined base fee:", ethers.formatUnits(minedBlock.baseFeePerGas, "gwei"), "gwei");
console.log("Effective gas price:", ethers.formatUnits(effectiveGasPrice, "gwei"), "gwei");
console.log("Actual priority fee:", ethers.formatUnits(actualPriorityFee, "gwei"), "gwei");
console.log("Actual fee:", ethers.formatEther(actualFee), "ETH");

Код отправляет type-2 транзакцию: современный формат Ethereum после EIP-1559.


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

latestBlock.baseFeePerGas — обязательная цена gas в текущем блоке. Её задаёт протокол, а не пользователь.

maxPriorityFeePerGas — чаевые валидатору за включение транзакции в блок.

maxFeePerGas — максимум, который отправитель готов заплатить за один gas.

type: 2 — явно указывает формат EIP-1559 транзакции.

receipt.gasPrice — фактическая цена gas после включения в блок. Для EIP-1559 это baseFeePerGas + priorityFeePerGas, но не выше maxFeePerGas.

receipt.gasUsed * receipt.gasPrice — итоговая комиссия, списанная с отправителя.


maxFeePerGas vs gasPrice

До EIP-1559 транзакция использовала gasPrice:

text
gasPrice = фиксированная цена одного gas
fee      = gasUsed × gasPrice

После EIP-1559 транзакция использует лимиты:

text
maxFeePerGas         = верхний предел цены gas
maxPriorityFeePerGas = максимум чаевых валидатору
fee                  = gasUsed × effectiveGasPrice

Главное отличие: gasPrice сразу задаёт цену, а maxFeePerGas задаёт потолок. Если фактическая цена ниже потолка, лишнее не списывается.


Из чего состоит комиссия

text
effectiveGasPrice = baseFeePerGas + priorityFeePerGas

baseFeePerGas сжигается протоколом. Она растёт, когда блоки перегружены, и падает, когда спрос ниже.

priorityFeePerGas получает валидатор. Чем выше чаевые, тем охотнее валидатор включит транзакцию при конкуренции за место в блоке.

maxFeePerGas должен покрывать обе части. Если он ниже текущей base fee, транзакция не может попасть в блок.


Когда нужен gasPrice

gasPrice нужен для legacy-транзакций type-0 и старых сетей без EIP-1559.

В Ethereum mainnet, Sepolia, Base, Arbitrum и Polygon обычно используйте EIP-1559 поля: maxFeePerGas и maxPriorityFeePerGas.

В ethers v6 текущие значения можно получить так:

typescript
const feeData = await provider.getFeeData();

console.log(feeData.gasPrice);
console.log(feeData.maxFeePerGas);
console.log(feeData.maxPriorityFeePerGas);

На legacy-сети maxFeePerGas может быть null. На EIP-1559 сети gasPrice остаётся доступен как совместимое значение, но для новой транзакции лучше задавать EIP-1559 поля.


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

Думать, что maxFeePerGas списывается полностью → списывается gasUsed × effectiveGasPrice, а не весь потолок.

Ставить maxFeePerGas ниже base fee → транзакция будет ждать или RPC отклонит её.

Смешивать gasPrice и EIP-1559 поля в одной транзакции → выберите legacy gasPrice или EIP-1559 maxFeePerGas / maxPriorityFeePerGas.


Что дальше

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