Розробка контрактів launchpad

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка контрактів launchpad
Складний
~1-2 тижні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1217
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    919
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1146
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    609
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    884

Розробка контракту 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 занадто високий.