Разработка контракта краудфандинга (ICO/IDO/IEO)
ICO, IDO и IEO — три разных механизма продажи токенов с разной архитектурой смарт-контрактов, разными требованиями к безопасности и разными правовыми рисками. Путать их на этапе проектирования — дорогостоящая ошибка.
ICO (Initial Coin Offering) — прямая продажа токенов из контракта. Полный контроль, никаких посредников, но и никаких гарантий для покупателей. Расцвет 2017–2018, сейчас ассоциируется с высоким риском мошенничества и регуляторным вниманием.
IDO (Initial DEX Offering) — продажа через DEX-механизм (Uniswap, PancakeSwap, Raydium). Ликвидность добавляется одновременно с продажей, цена определяется рынком или через специализированную launchpad платформу.
IEO (Initial Exchange Offering) — продажа через централизованную биржу. Биржа выступает посредником и KYC провайдером. Смарт-контракт в этом случае — упрощённый, основная логика на стороне биржи.
Структура crowdsale контракта
Базовая архитектура, применимая для большинства продаж:
contract TokenSale {
using SafeERC20 for IERC20;
IERC20 public immutable token;
address public immutable treasury;
// Конфигурация раундов
struct Round {
uint256 price; // wei за 1 token (с учётом decimals)
uint256 allocation; // всего токенов в раунде
uint256 sold;
uint256 minPurchase;
uint256 maxPurchase; // per wallet cap
uint256 startTime;
uint256 endTime;
bool whitelistRequired;
}
Round[] public rounds;
uint256 public activeRound;
mapping(address => uint256) public purchased; // total per wallet
mapping(address => bool) public whitelist;
mapping(address => bool) public claimed;
// Vesting: токены выдаются не сразу
uint256 public tgePercent; // % сразу при TGE
uint256 public cliffEnd; // timestamp конца cliff периода
uint256 public vestingEnd; // timestamp конца vesting
event TokensPurchased(address indexed buyer, uint256 ethAmount, uint256 tokenAmount, uint256 round);
event TokensClaimed(address indexed claimant, uint256 amount);
}
Вычисление количества токенов
Частая ошибка: неправильная обработка decimals. Если ETH имеет 18 decimals, а токен — тоже 18, формула тривиальна. Но если токен имеет 6 decimals (USDC-style) или 0 (редкий случай) — расчёт другой.
function calculateTokens(uint256 ethAmount, uint256 roundIndex) public view returns (uint256) {
Round storage round = rounds[roundIndex];
// price хранится как wei ETH за 1 полный токен (с учётом token decimals)
// Например: если 1 token = 0.001 ETH, то price = 0.001 * 1e18 = 1e15
return (ethAmount * 10**token.decimals()) / round.price;
}
Whitelist и KYC
Для IDO на launchpad-платформах — whitelist через merkle proof (экономия gas на хранении):
bytes32 public whitelistMerkleRoot;
function purchaseWithProof(bytes32[] calldata proof) external payable {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(proof, whitelistMerkleRoot, leaf),
"Not whitelisted"
);
_purchase();
}
Обновление корня Merkle-дерева при добавлении новых адресов — off-chain операция (generateMerkleTree + setMerkleRoot on-chain). Важно: при обновлении root старые proof инвалидируются — нужна либо плавная миграция, либо хранение нескольких root-ов для перекрывающихся окон.
Vesting механизм
Продажа без vesting — красный флаг для инвесторов. Стандартная схема: 10% TGE (token generation event) + 6 месяцев cliff + 18 месяцев линейный vesting.
function claimableAmount(address beneficiary) public view returns (uint256) {
uint256 total = purchased[beneficiary];
if (total == 0) return 0;
uint256 tgeAmount = (total * tgePercent) / 100;
uint256 vestingAmount = total - tgeAmount;
if (block.timestamp < cliffEnd) {
// Только TGE часть доступна (если TGE уже прошёл)
return tgeReleased[beneficiary] ? 0 : tgeAmount;
}
if (block.timestamp >= vestingEnd) {
return total - claimed[beneficiary]; // всё
}
// Линейный vesting после cliff
uint256 elapsed = block.timestamp - cliffEnd;
uint256 duration = vestingEnd - cliffEnd;
uint256 vestedAmount = (vestingAmount * elapsed) / duration;
uint256 totalClaimable = tgeAmount + vestedAmount;
return totalClaimable - claimed[beneficiary];
}
Риски и защиты
Front-running при старте продажи. MEV-боты отслеживают mempool и вставляют транзакции в первый блок продажи. Для fair launch: commit-reveal схема или randomized start block.
Reentrancy при возврате ETH. Если в логику включён refund (например, при недостижении softcap), функция возврата должна использовать checks-effects-interactions pattern и ReentrancyGuard.
Manupulation с ценой через большой purchase. При bonding curve моделях (цена растёт с каждой покупкой) — возможна манипуляция через фиктивные покупки с последующей перепродажей. Решение: минимальный lock period или фиксированная цена в раунде.
Owner privilege abuse. Функции setPrice(), withdraw(), pause() должны иметь либо timelock, либо multisig (Gnosis Safe). Бесконтрольный owner — причина большинства rugpull сценариев.
Softcap и refund механизм. Если не собрали minimum — покупатели должны получить ETH обратно. Стандартный паттерн: хранить contributions в mapping, pull-pattern для возврата (не push), активировать режим refund через функцию после finalization.
Тестирование и аудит
Foundry fuzzing обязателен для crowdsale контрактов:
function testFuzz_purchaseCalculation(uint256 ethAmount, uint256 decimals) public {
ethAmount = bound(ethAmount, 0.001 ether, 100 ether);
decimals = bound(decimals, 0, 18);
// Проверяем, что не бывает overflow при разных комбинациях
uint256 tokens = sale.calculateTokens(ethAmount, 0);
assertGt(tokens, 0, "Zero tokens for non-zero ETH");
}
Ключевые инварианты для fuzzing:
-
SUM(purchased) <= total allocation— никогда не продаём больше, чем есть -
SUM(claimed) <= SUM(purchased)— никогда не выдаём больше, чем продали - После finalization и refund mode:
contract balance >= SUM(contributions для unfulfilled покупателей)
Для продаж с существенным объёмом (от $500K) — внешний аудит обязателен. Минимум одна команда из Tier 2 аудиторов (Pessimistic, MixBytes, Oxorio).
Сроки
Стандартный crowdsale контракт с vesting и merkle whitelist — 5–7 рабочих дней разработки + 2–3 дня тестирования. С нестандартной логикой (bonding curve, multi-currency, dynamic rounds) — 2–3 недели.







