Разработка системы голландского аукциона для токенов
Классический токен-сейл с фиксированной ценой создаёт одну из двух проблем: либо цена слишком низкая и все токены уходят в первые секунды (gas war, несправедливое распределение в пользу MEV-ботов), либо слишком высокая — и сейл не заполняется. Голландский аукцион решает обе проблемы за счёт механизма ценообразования: цена начинается высокой и снижается до тех пор, пока спрос не встретит предложение. Цена сейла — это рыночный клиринговый уровень, а не произвольное число из whitepaper.
Именно так Paradigm и a16z проводили первые крупные токен-дистрибьюции в DeFi-пространстве. Gnosis Protocol использует вариацию этого механизма для batch аукционов.
Механика контракта
Линейное vs экспоненциальное снижение цены
Простейшая реализация — линейная функция:
price(t) = startPrice - (startPrice - endPrice) * (t - startTime) / duration
Проблема линейной модели: большую часть времени цена снижается медленно, потому что равномерна во всём диапазоне. Участники ждут минимума — аукцион не заполняется в середине, все пытаются купить в конце.
Экспоненциальное снижение более реалистично описывает ценовое поведение рынка:
function getCurrentPrice() public view returns (uint256) {
if (block.timestamp <= startTime) return startPrice;
if (block.timestamp >= endTime) return endPrice;
uint256 elapsed = block.timestamp - startTime;
uint256 duration = endTime - startTime;
// Экспоненциальное снижение через 18-decimal fixed point
// price = startPrice * e^(-k * t/T)
// Аппроксимируем через integer arithmetic
uint256 priceDelta = startPrice - endPrice;
uint256 decayFactor = PRBMath.exp(-int256(decayRate * elapsed / duration));
return endPrice + priceDelta * decayFactor / 1e18;
}
На практике большинство production Dutch Auction контрактов используют дискретные шаги снижения (step-down) вместо непрерывной функции — это проще для понимания участниками и дешевле в газе.
Commit-reveal для защиты от MEV
В стандартном Dutch Auction все видят текущую цену, и как только она становится "справедливой", все пытаются купить одновременно. MEV-боты front-run реальных покупателей, выставляя более высокий gas price. Результат: газовая война, но уже при клиринговой цене вместо стартовой — немного лучше, но не идеально.
Решение — commit-reveal в Dutch Auction: участники отправляют encrypted commitment (hash от суммы и salt) без раскрытия намерения. После завершения фазы commitment — reveal фаза. Клиринговая цена рассчитывается по совокупному спросу.
Это сложнее в реализации, но устраняет front-running полностью. GnosisDAO использовал подобную схему для своих аукционов.
Ключевые контрактные параметры
struct AuctionConfig {
uint256 startPrice; // Максимальная цена (например, 1 ETH за токен)
uint256 endPrice; // Минимальная цена (например, 0.1 ETH)
uint256 startTime; // Unix timestamp начала
uint256 endTime; // Unix timestamp конца
uint256 totalTokens; // Количество токенов на продажу
uint256 minBidAmount; // Минимальная покупка
bool allowWhitelist; // Ограничить до whitelist
bytes32 merkleRoot; // Merkle root для whitelist
}
Whitelist через Merkle Proof
Если аукцион ограничен для определённых адресов:
function bid(uint256 amount, bytes32[] calldata merkleProof) external payable {
if (config.allowWhitelist) {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(merkleProof, config.merkleRoot, leaf),
"Not whitelisted"
);
}
uint256 currentPrice = getCurrentPrice();
uint256 tokenAmount = msg.value * 1e18 / currentPrice;
require(tokenAmount >= config.minBidAmount, "Below minimum");
require(tokensSold + tokenAmount <= config.totalTokens, "Exceeds supply");
tokensSold += tokenAmount;
bids[msg.sender] += tokenAmount;
emit BidPlaced(msg.sender, tokenAmount, currentPrice, msg.value);
}
Возврат переплаты: клиринговая цена
В классическом Dutch Auction участники платят цену момента покупки. В Fair Dutch Auction (DutchX, Gnosis) — все платят одну клиринговую цену, даже те, кто купил раньше по более высокой. Разница возвращается.
Это справедливее, но сложнее в реализации: нужно дождаться конца аукциона, рассчитать клиринговую цену, и дать каждому участнику забрать refund через отдельный claim.
function claim() external {
require(auctionEnded, "Auction not ended");
uint256 userBid = bids[msg.sender];
require(userBid > 0, "No bid");
uint256 paid = payments[msg.sender];
uint256 cost = userBid * clearingPrice / 1e18;
uint256 refund = paid - cost;
bids[msg.sender] = 0;
payments[msg.sender] = 0;
// Переводим токены
token.transfer(msg.sender, userBid);
// Возвращаем переплату
if (refund > 0) {
(bool success, ) = msg.sender.call{value: refund}("");
require(success, "Refund failed");
}
}
Типичные ошибки и уязвимости
Ошибки расчёта клиринговой цены. Если tokensSold не достиг totalTokens — аукцион закрылся по endPrice. Если достиг раньше — клиринговая цена это цена момента заполнения. Контракт должен корректно обрабатывать оба сценария, иначе либо пользователи не получат refund, либо контракт отдаст больше токенов, чем должен.
Округление в пользу контракта. При делении wei на цену возникают remainder. Всегда округляем количество токенов вниз, сохраняем dust как treasury или включаем в burn-механизм.
Reentrancy в claim(). ETH-refund перед или вместе с transfer токенов — классическая точка reentrancy. Обновляем bids[msg.sender] = 0 до любых внешних вызовов.
Отсутствие паузы. Если обнаружена ошибка во время аукциона — нужна экстренная остановка. Функция pause() с multisig-контролем, которая замораживает новые bids, но не блокирует claim для уже сделанных.
Сроки разработки: 3-5 рабочих дней для базового Dutch Auction, до 2 недель для Fair Dutch Auction с commit-reveal. Стоимость рассчитывается индивидуально.







