Розробка контрактів аукціонів на блокчейні
Аукціон на блокчейні — це не просто "приймай ставки та визначай переможця". Відкритість стану EVM робить механіку торгів вразливою до маніпуляцій, які в традиційних системах неможливі. Front-running ставок через MEV, газові війни в останню секунду, griefing через блокування ETH у аутбіддерів — кожен з цих векторів потребує архітектурної відповіді, а не просто перевірки в require.
Механіки аукціонів: English vs Dutch
English Auction (зростаюча ціна)
Класика: ціна зростає, перемагає остання ставка. Для 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 (спадаюча ціна)
Ціна починається високо та знижується зі часом. Учасник платить поточну ціну й одразу отримує актив. Використовується для token-сейлів (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 тижні.







