Розробка контракту launchpad
Launchpad — це не просто «контракт який продає токени». Якби задача зводилася до цього, хватило б 50 рядків Solidity. Реальна задача — система, яка одночасно керує whitelist учасниками, кількома раундами з різними цінами та лімітами, vesting розписанням для аллокованих токенів, refund механікою при невиконанні softcap, та anti-whale логікою. Плюс необхідність пережити хоча б один аудит без критичних знахідок. Подивимось що це означає на практиці.
Архітектура многораундового launchpad
Структура раундів
Типичний launchpad має кілька послідовних раундів:
Seed Round → Private Round → Public Round (IDO)
[whitelist] [whitelist] [открытий / гарантований + FCFS]
[лімітcюрограМ $500] [лімітовки $2000] [ліміт $300 / no limit FCFS]
[$0.05/token] [$0.08/token] [$0.12/token]
У смарт-контракті це представляється через конфігуровані раунди:
struct Round {
uint256 startTime;
uint256 endTime;
uint256 price; // у stablecoin (6 decimals для USDC)
uint256 minAllocation; // мінімальна покупка
uint256 maxAllocation; // максимальна покупка на адресу
uint256 totalCap; // максимум для всього раунду
uint256 raised; // вже зібрано
bytes32 merkleRoot; // whitelist через Merkle tree
bool requiresKYC; // флаг KYC верифікації
bool isActive;
}
Merkle tree для whitelist — стандартний паттерн. Альтернатива (mapping of approved addresses) не масштабується: 10k адрес у маппінгу — 10k транзакцій для заповнення. Merkle proof дозволяє верифікувати участь без зберігання всього списку on-chain.
function participate(
uint256 roundId,
uint256 amount,
bytes32[] calldata merkleProof
) external nonReentrant whenNotPaused {
Round storage round = rounds[roundId];
require(block.timestamp >= round.startTime, "Not started");
require(block.timestamp < round.endTime, "Ended");
require(round.raised + amount <= round.totalCap, "Cap exceeded");
// Верифікація whitelist через Merkle proof
if (round.merkleRoot != bytes32(0)) {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(merkleProof, round.merkleRoot, leaf),
"Not whitelisted"
);
}
// KYC перевірка через on-chain реєстр
if (round.requiresKYC) {
require(kycRegistry.isVerified(msg.sender), "KYC required");
}
uint256 newTotal = contributions[roundId][msg.sender] + amount;
require(newTotal >= round.minAllocation, "Below minimum");
require(newTotal <= round.maxAllocation, "Exceeds maximum");
contributions[roundId][msg.sender] = newTotal;
round.raised += amount;
// Приймаємо оплату (USDC)
paymentToken.transferFrom(msg.sender, address(this), amount);
emit Participated(msg.sender, roundId, amount);
}
Vesting: головна технічна складність
Більшість аудитних знахідок у launchpad контрактах — саме в vesting логіці. Не тому що вона принципово складна, а тому що edge cases погано тестуються.
Linear vesting з cliff
struct VestingSchedule {
uint256 totalAmount; // всього токенів до отримання
uint256 cliffEnd; // до цього часу — нічого
uint256 vestingStart; // початок лінійного vesting (зазвичай = cliffEnd)
uint256 vestingEnd; // кінець vesting періоду
uint256 claimed; // уже отримано
bool revocable; // можна ли відозвати (для команди, не для інвесторів)
}
function claimable(address beneficiary) public view returns (uint256) {
VestingSchedule memory schedule = vestingSchedules[beneficiary];
if (block.timestamp < schedule.cliffEnd) {
return 0;
}
uint256 elapsed = block.timestamp - schedule.vestingStart;
uint256 total = schedule.vestingEnd - schedule.vestingStart;
uint256 vested = elapsed >= total
? schedule.totalAmount
: (schedule.totalAmount * elapsed) / total;
return vested - schedule.claimed;
}
function claim() external nonReentrant {
uint256 amount = claimable(msg.sender);
require(amount > 0, "Nothing to claim");
vestingSchedules[msg.sender].claimed += amount;
projectToken.transfer(msg.sender, amount);
emit TokensClaimed(msg.sender, amount);
}
Типичні помилки в vesting
Проблема 1: TGE (Token Generation Event) процент — частина токенів розблокується одразу при TGE, решта vest'ється. Неопитні розробники реалізують як окремий claim, користувачі часто забувають. Правильно — TGE частина включається в claimable() розрахунок автоматично з моменту TGE.
Проблема 2: Revoke при збереженні заробленого — якщо vesting для команди revocable, при відзиві повинні зберігатися вже заробленні токени. Тільки незаробленна частина повертається проекту.
Проблема 3: Precision loss при діленні — (totalAmount * elapsed) / total при маленьких amounts та великому total може давати 0. Порядок операцій важливий: завжди множимо перед діленням.
Softcap / Hardcap та механізм refund
uint256 public softcap; // мінімум для успішного IDO
uint256 public hardcap; // максимум
enum SaleStatus { Active, SoftcapMet, HardcapMet, Failed, Finalized }
SaleStatus public status;
function finalizeSale() external onlyOwner {
require(block.timestamp > saleEndTime, "Sale not ended");
uint256 totalRaised = getTotalRaised();
if (totalRaised < softcap) {
status = SaleStatus.Failed;
// Активуємо режим refund
emit SaleFailed(totalRaised, softcap);
} else {
status = SaleStatus.Finalized;
// Переводимо funds у treasury
paymentToken.transfer(treasury, totalRaised);
// Включаємо vesting claimability
vestingStartTime = block.timestamp + TGE_DELAY;
emit SaleFinalized(totalRaised);
}
}
function refund(uint256 roundId) external nonReentrant {
require(status == SaleStatus.Failed, "Sale not failed");
uint256 contribution = contributions[roundId][msg.sender];
require(contribution > 0, "Nothing to refund");
contributions[roundId][msg.sender] = 0;
paymentToken.transfer(msg.sender, contribution);
emit Refunded(msg.sender, roundId, contribution);
}
Anti-whale та fairness механіки
Max allocation per wallet — базова захист, реалізована через maxAllocation у раунді.
Anti-bot захист в FCFS раунді — First-Come-First-Served раунди атакуються ботами. Поширені захисти:
- Whitelist навіть для public раунду: всі, хто зареєструвався до певного часу, отримують гарантовану аллокацію. FCFS тільки для незареєстрованих.
- Commit-reveal: учасник спочатку робить commit (хеш від наміру купити), reveal відбувається в окремій транзакції через N блоків. Боти втрачають перевагу моментального реагування.
- Dutch auction замість фіксованої ціни: ціна стартує високою та убуває. Ринковий механізм сам знаходить рівноважну ціну, anti-bot захист вбудований.
Anti-sniper на старт раунду:
modifier antiSnipe(uint256 roundId) {
Round storage round = rounds[roundId];
// Перші 30 секунд — тільки whitelist tier 1 (OG учасники)
if (block.timestamp < round.startTime + 30) {
require(tier1Whitelist[msg.sender], "OG round");
}
_;
}
Інтеграція з токен контрактом
Launchpad не видає токени одразу — записує аллокації, токени видаються через vesting. Це вимагає або pre-mint токенів на launchpad, або mint-on-claim механізм:
// Вариант 1: pre-funded
// Перш ніж почнеться IDO project team переводить потрібну кількість токенів
// на launchpad контракт
projectToken.transferFrom(projectOwner, address(this), totalTokensForSale);
// Вариант 2: mint-on-claim (якщо токен дає MINTER_ROLE лаунчпаду)
function claim() external nonReentrant {
uint256 amount = claimable(msg.sender);
require(amount > 0, "Nothing to claim");
vestingSchedules[msg.sender].claimed += amount;
IProjectToken(projectToken).mint(msg.sender, amount); // mint замість transfer
emit TokensClaimed(msg.sender, amount);
}
Вариант 2 популярніше для проектів, де total supply не фіналізований до закриття IDO.
Тестування та аудит
Для launchpad контракту обов'язковий fork testing — тести на форці mainnet з реальними USDC адресами:
forge test --fork-url $ETH_RPC -vvv --match-contract LaunchpadTest
Fuzz тестування для vesting:
function testFuzz_vestingClaimable(
uint256 totalAmount,
uint256 elapsed,
uint256 duration
) public {
totalAmount = bound(totalAmount, 1e6, 1e30); // розумні границі
duration = bound(duration, 1 days, 4 * 365 days);
elapsed = bound(elapsed, 0, duration);
// Інваріант: claimable ніколи не перевищує totalAmount
uint256 vested = (totalAmount * elapsed) / duration;
assertLe(vested, totalAmount);
}
Критичні сценарії для ручного тестування: refund після failed sale з кількома раундами; claim при частково відозваному vesting; участь через contract wallet (не EOA) — FOMO.finance hack 2021 був саме такий exploit.
Терміни: повноцінний launchpad контракт з аудитом — 8–14 тижнів. Без аудиту до production не виходимо — TVL launchpad занадто високий.







