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

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка контрактов launchpad
Сложная
~1-2 недели
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка контракта 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 слишком высок.