китаев.tech

Фронтраннинг — как работает и защита

Как работает фронтраннинг в Ethereum, почему публичные параметры транзакции опасны и как защитить аукцион через commit-reveal.

Что такое фронтраннинг

Фронтраннинг — это атака, при которой другой участник видит вашу транзакцию до включения в блок и отправляет свою транзакцию с более высоким приоритетом.

В Ethereum транзакция до подтверждения попадает в mempool. Если она раскрывает выгодное действие — ставку, цену, swap, liquidation — бот может скопировать идею и попасть в блок раньше.


Пишем уязвимый и защищённый аукцион

Создайте файл contracts/FrontrunningDemo.sol:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract PublicAuction {
    address public leader;
    uint256 public highestBid;

    error BidTooLow();

    function bid() external payable {
        if (msg.value <= highestBid) {
            revert BidTooLow();
        }

        leader = msg.sender;
        highestBid = msg.value;
    }
}

contract CommitRevealAuction {
    struct Bid {
        bytes32 commitment;
        uint256 deposit;
        bool revealed;
    }

    mapping(address => Bid) public bids;

    address public leader;
    uint256 public highestBid;

    error AlreadyCommitted();
    error NoCommitment();
    error InvalidReveal();
    error BidAlreadyRevealed();
    error DepositTooSmall();
    error BidTooLow();

    function commit(bytes32 commitment) external payable {
        if (bids[msg.sender].commitment != bytes32(0)) {
            revert AlreadyCommitted();
        }

        bids[msg.sender] = Bid({
            commitment: commitment,
            deposit: msg.value,
            revealed: false
        });
    }

    function reveal(uint256 amount, bytes32 salt) external {
        Bid storage userBid = bids[msg.sender];

        if (userBid.commitment == bytes32(0)) {
            revert NoCommitment();
        }

        if (userBid.revealed) {
            revert BidAlreadyRevealed();
        }

        bytes32 expectedCommitment = keccak256(
            abi.encodePacked(msg.sender, amount, salt)
        );

        if (userBid.commitment != expectedCommitment) {
            revert InvalidReveal();
        }

        if (userBid.deposit < amount) {
            revert DepositTooSmall();
        }

        if (amount <= highestBid) {
            revert BidTooLow();
        }

        userBid.revealed = true;
        leader = msg.sender;
        highestBid = amount;
    }
}

Создайте файл scripts/frontrunning.ts:

typescript
import { network } from "hardhat";

const { ethers } = await network.create();

const [alice, bot] = await ethers.getSigners();

const publicAuction = await ethers.deployContract("PublicAuction");
await publicAuction.waitForDeployment();

await publicAuction.connect(alice).bid({
  value: ethers.parseEther("1"),
});

// Бот видит ставку Alice и перебивает её до попадания в блок.
await publicAuction.connect(bot).bid({
  value: ethers.parseEther("1.01"),
});

console.log("PublicAuction leader:", await publicAuction.leader());
console.log("PublicAuction highestBid:", ethers.formatEther(await publicAuction.highestBid()));

const sealedAuction = await ethers.deployContract("CommitRevealAuction");
await sealedAuction.waitForDeployment();

const aliceSalt = ethers.encodeBytes32String("alice-secret");
const botSalt = ethers.encodeBytes32String("bot-secret");

const aliceAmount = ethers.parseEther("1");
const botAmount = ethers.parseEther("0.9");

const aliceCommitment = ethers.solidityPackedKeccak256(
  ["address", "uint256", "bytes32"],
  [alice.address, aliceAmount, aliceSalt],
);

const botCommitment = ethers.solidityPackedKeccak256(
  ["address", "uint256", "bytes32"],
  [bot.address, botAmount, botSalt],
);

await sealedAuction.connect(alice).commit(aliceCommitment, {
  value: aliceAmount,
});

await sealedAuction.connect(bot).commit(botCommitment, {
  value: botAmount,
});

await sealedAuction.connect(bot).reveal(botAmount, botSalt);
await sealedAuction.connect(alice).reveal(aliceAmount, aliceSalt);

console.log("CommitRevealAuction leader:", await sealedAuction.leader());
console.log("CommitRevealAuction highestBid:", ethers.formatEther(await sealedAuction.highestBid()));

Запуск:

bash
npx hardhat run scripts/frontrunning.ts

В PublicAuction бот видит сумму и перебивает её. В CommitRevealAuction сумма скрыта хешем до reveal-фазы, поэтому бот не может узнать ставку из commit-транзакции.


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

mempool — очередь неподтверждённых транзакций. Валидаторы и боты видят calldata, msg.value, gas-параметры и адрес вызываемого контракта.

bid() в PublicAuction — уязвимая функция. Она сразу раскрывает размер ставки через msg.value.

commit(bytes32 commitment) — сохраняет только хеш ставки. По нему нельзя восстановить amount и salt, если salt достаточно случайный.

reveal(uint256 amount, bytes32 salt) — раскрывает ставку позже. Контракт заново считает хеш и сравнивает его с сохранённым commitment.

abi.encodePacked(msg.sender, amount, salt) — привязывает commitment к конкретному адресу. Без msg.sender другой участник мог бы раскрыть чужую ставку.


Защита в разных сценариях

Для аукционов и голосований используйте commit-reveal: сначала скрытое обязательство, потом раскрытие.

Для swap и DeFi-операций задавайте slippage, deadline и минимальный результат. Это не убирает фронтраннинг полностью, но ограничивает ущерб.

Для крупных сделок используйте private transaction relays или RFQ-механики, где транзакция не попадает в публичный mempool до включения в блок.


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

Передавать цену или ставку открытым параметром → если значение даёт преимущество, скрывайте его до момента исполнения.

Использовать слабый saltsalt = bytes32("1") можно перебрать. Генерируйте случайный bytes32 вне сети.

Думать, что высокий gas защищает → высокий priority fee помогает попасть раньше, но не скрывает содержание транзакции.


Что дальше

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