Разработка контрактов аукционов на блокчейне
Аукцион на блокчейне — это не просто «принимай ставки и определяй победителя». Открытость состояния EVM делает механику торгов уязвимой к манипуляциям, которые в традиционных системах невозможны. Front-running ставок через MEV, газовые войны в последнюю секунду, griefing через блокировку ETH у аутбиддеров — каждый из этих векторов требует архитектурного ответа, а не просто проверки в require.
Механики аукционов: English vs Dutch
English Auction (ascending price)
Классика: цена растёт, побеждает последняя ставка. Для NFT и токенов — наиболее распространённый формат. Основная техническая проблема — last-minute sniping и front-running.
В Ethereum-аукционах без защиты бот видит транзакцию финальной ставки в mempool и вставляет свою с большим gas priority fee. Решение — time extension: если ставка поступает в последние N минут до дедлайна, аукцион продлевается автоматически:
if (block.timestamp > auctionEnd - timeBuffer) {
auctionEnd = block.timestamp + timeBuffer;
emit AuctionExtended(auctionId, auctionEnd);
}
timeBuffer обычно 10-15 минут. Именно так устроен аукцион Nouns DAO — один из наиболее технически корректных публичных реализаций.
Dutch Auction (descending price)
Цена начинается высоко и снижается со временем. Участник платит текущую цену и немедленно получает актив. Используется для токен-сейлов (Gnosis Protocol, некоторые NFT-дропы).
Ключевой параметр — кривая снижения цены. Линейная кривая:
function getCurrentPrice() public view returns (uint256) {
if (block.timestamp >= endTime) return reservePrice;
uint256 elapsed = block.timestamp - startTime;
uint256 totalDuration = endTime - startTime;
uint256 priceDrop = startPrice - reservePrice;
return startPrice - (priceDrop * elapsed / totalDuration);
}
Экспоненциальная кривая более реалистична для рыночного ценообразования, но дороже по газу из-за exp() — обычно аппроксимируем через lookup table или piecewise linear.
Проблемы, которые решаем
Газовые войны и griefing через refund
В стандартной реализации English Auction предыдущая ставка возвращается при аутбиддинге:
// Опасный паттерн
payable(previousBidder).transfer(previousBid);
Если previousBidder — контракт с fallback, который всегда revert, весь аукцион блокируется. Это классический DoS через gas griefing.
Решение — pull payment pattern: вместо автоматического возврата храним pending withdrawals в mapping и даём пользователю самостоятельно забрать ETH:
mapping(address => uint256) public pendingReturns;
function bid() external payable {
// ...
pendingReturns[previousBidder] += previousBid;
// новая ставка принята, старая не возвращается автоматически
}
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
if (amount == 0) revert NothingToWithdraw();
pendingReturns[msg.sender] = 0; // обнуляем до transfer (reentrancy guard)
(bool ok,) = payable(msg.sender).call{value: amount}("");
if (!ok) revert TransferFailed();
}
Commitment scheme против front-running
Для аукционов, где важна секретность ставок до закрытия (sealed-bid auction), используется commit-reveal:
-
Commit phase: участник отправляет
keccak256(abi.encode(bid, salt, address))— хэш ставки - Reveal phase: участник раскрывает реальную ставку и salt, контракт проверяет хэш
- Победитель определяется только после reveal
Ограничение: участник может не раскрыть ставку, если понимает, что проиграл. Решение — залог при commit, который сгорает при неявке на reveal (anti-griefing bond).
Reentrancy в многолотовых аукционах
При параллельных аукционах (маркетплейс с множеством лотов) особенно опасна reentrancy через ETH-возврат. Используем ReentrancyGuard от OpenZeppelin или паттерн checks-effects-interactions строго:
// Checks
require(bid > currentHighestBid + minBidIncrement);
// Effects — обновляем state ДО внешних вызовов
highestBid = bid;
highestBidder = msg.sender;
pendingReturns[previousBidder] += previousAmount;
// Interactions — только после
emit BidPlaced(msg.sender, bid);
Стек и инструменты
Разрабатываем на Solidity 0.8.x с Foundry. Тестируем fork mainnet через vm.createFork — это позволяет проверять взаимодействие с реальными NFT-контрактами (ERC-721, ERC-1155) и Chainlink price feeds для деноминации ставок.
Fuzzing через forge fuzz обязателен для функций с ценовыми расчётами — особенно для Dutch Auction с кривыми снижения, где есть риск integer overflow при крайних значениях timestamp.
Аукционы для NFT стандартно поддерживают ERC-721 и ERC-1155 через IERC721.safeTransferFrom / IERC1155.safeTransferFrom. Контракт аукциона выступает escrow — держит NFT с момента листинга до завершения и передаёт победителю.
Процесс работы
Аналитика. Определяем тип аукциона, активы (NFT/токены/реальные активы), целевой чейн (Ethereum mainnet, Polygon, Arbitrum), требования к конфиденциальности ставок.
Проектирование. Выбираем механику возврата ставок (pull vs push), anti-griefing механизмы, параметры time extension. Если несколько типов аукционов — проектируем модульную архитектуру с базовым контрактом.
Разработка и тестирование. Foundry-тесты с 95%+ coverage. Обязательно: fork-тесты с реальным mainnet состоянием, fuzz-тесты на ценовые функции, invariant tests для проверки инвариантов (сумма всех pending returns ≤ баланс контракта).
Деплой. Верификация на Etherscan/Polygonscan. Если аукцион для NFT-маркетплейса — интеграция с фронтендом через wagmi/viem.
Ориентиры по срокам
Одиночный аукционный контракт (English или Dutch): 3-5 дней включая тесты. Мультилотовый маркетплейс с обоими типами аукционов и commit-reveal: 2-3 недели.







