Розробка контракту краудфандинга (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; // всього на гаманець
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
Для launchpad IDO — 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 — червоний флаг для інвесторів. Стандартна схема: 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.
Маніпуляція ціною через великий purchase. У bonding curve моделях (ціна зростає з кожною покупкою) — можлива маніпуляція через фіктивні покупки з подальшою перепродажею. Рішення: мінімальний lock period або фіксована ціна у раунді.
Зловживання привілеями owner. Функції setPrice(), withdraw(), pause() повинні мати або timelock, або multisig (Gnosis Safe). Бесконтрольний owner — причина більшості rugpull сценаріїв.
Softcap та механізм refund. Якщо мінімум не зібраний — покупці повинні отримати ETH назад. Стандартний паттерн: зберігати contributions у маппінгу, pull-pattern для повернення (не push), активувати режим refund через функцію після фіналізації.
Тестування та аудит
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)— ніколи не видаємо більше, ніж продали - Після фіналізації та режиму refund:
contract balance >= SUM(contributions для невиконаних покупців)
Для продаж із значним обсягом (від $500K) — зовнішній аудит обов'язковий. Мінімум одна команда Tier 2 аудиторів (Pessimistic, MixBytes, Oxorio).
Графік
Стандартний crowdsale контракт з vesting та merkle whitelist — 5–7 робочих днів розробки + 2–3 дні тестування. З нестандартною логікою (bonding curve, multi-currency, dynamic rounds) — 2–3 тижні.







