Разработка контракта launchpad
Launchpad — это не просто "контракт который продаёт токены". Если бы задача сводилась к этому, хватило бы 50 строк Solidity. Реальная задача — система, которая одновременно управляет whitelist участников, несколькими раундами с разными ценами и лимитами, vesting расписанием для аллокированных токенов, refund механикой при недостижении softcap, и anti-whale логикой. Плюс необходимость пережить хотя бы один аудит без критических находок. Посмотрим что это означает на практике.
Архитектура многораундового launchpad
Структура раундов
Типичный launchpad имеет несколько последовательных раундов:
Seed Round → Private Round → Public Round (IDO)
[whitelist] [whitelist] [открытый / гарантированный + FCFS]
[лимит $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 позволяет верифицировать участие в whitelist без хранения всего списка 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 registry
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 protection в FCFS раунде — First-Come-First-Served раунды атакуются ботами. Распространённые защиты:
- Whitelist даже для public раунда: все, кто зарегистрировался до определённого времени, получают guaranteed allocation. FCFS только для незарегистрированных.
- Commit-reveal: участник сначала делает commit (хэш от намерения купить), reveal происходит в отдельной транзакции через N блоков. Боты теряют преимущество моментального реагирования.
- Dutch auction вместо фиксированной цены: цена стартует высокой и убывает. Рыночный механизм сам находит равновесную цену, анти-бот защита встроена.
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 слишком высок.







