Разработка системы airdrop-кампаний

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1Все 1306 услуг
Разработка системы airdrop-кампаний
Средний
~3-5 дней
Часто задаваемые вопросы

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

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

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1286
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    902
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1122
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    589
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    859

Разработка системы airdrop-кампании

Технически airdrop — это распределение токенов. На практике — это маркетинговый инструмент, который либо создаёт долгосрочных участников протокола, либо генерирует одноразовую волну sell pressure. Разница определяется не суммой распределяемых токенов, а тем, кто их получает и на каких условиях.

Merkle Distributor: стандарт для массового распределения

Наивный подход — вызвать transfer на каждый адрес. При 100,000 получателей это 100,000 транзакций, огромный gas и точка отказа. Правильный подход — Merkle Distributor, где получатели сами клеймят свои токены.

Off-chain: формируем список (address → amount), строим Merkle tree, публикуем root в контракт.

On-chain: пользователь предоставляет Merkle proof, контракт верифицирует и выдаёт токены.

contract MerkleDistributor {
    address public immutable token;
    bytes32 public immutable merkleRoot;

    // Bitfield для отслеживания claimed — экономия gas vs mapping(address => bool)
    mapping(uint256 => uint256) private claimedBitMap;

    function isClaimed(uint256 index) public view returns (bool) {
        uint256 claimedWordIndex = index / 256;
        uint256 claimedBitIndex = index % 256;
        uint256 claimedWord = claimedBitMap[claimedWordIndex];
        uint256 mask = (1 << claimedBitIndex);
        return claimedWord & mask == mask;
    }

    function _setClaimed(uint256 index) private {
        uint256 claimedWordIndex = index / 256;
        uint256 claimedBitIndex = index % 256;
        claimedBitMap[claimedWordIndex] |= (1 << claimedBitIndex);
    }

    function claim(
        uint256 index,
        address account,
        uint256 amount,
        bytes32[] calldata merkleProof
    ) external {
        require(!isClaimed(index), "Already claimed");

        // Верификация proof
        bytes32 node = keccak256(abi.encodePacked(index, account, amount));
        require(
            MerkleProof.verify(merkleProof, merkleRoot, node),
            "Invalid proof"
        );

        _setClaimed(index);
        IERC20(token).safeTransfer(account, amount);
        emit Claimed(index, account, amount);
    }
}

Bitfield vs mapping: хранение claimed status в packed bitfield (256 статусов в одном uint256 slot) экономит ~80% gas на SSTORE/SLOAD по сравнению с mapping(address => bool).

Типы airdrop и их применение

Retroactive airdrop

Распределение для существующих пользователей протокола — самый эффективный тип. Uniswap UNI airdrop, Arbitrum ARB, Optimism OP — все были retroactive для ранних пользователей.

Критерии eligibility определяются on-chain анализом:

  • Объём транзакций за период
  • Количество уникальных контрактов, с которыми взаимодействовал адрес
  • Давность первой транзакции
  • Удержание позиций (не just-in-time farming)

Sybil filtering — главная техническая задача. Один человек с 1000 адресов не должен получить в 1000 раз больше.

Признаки Sybil-кластеров:

  • Адреса получают ETH с одного funding source
  • Транзакции с одинаковыми паттернами (одно и то же время суток, одни и те же протоколы)
  • Пустые адреса между действиями (gas station pattern)
  • Минимальные транзакции для выполнения минимальных критериев

Инструменты Sybil detection: Chainanalysis Sybil (платный), собственный SQL анализ по onchain данным через Dune Analytics или собственный indexed node.

Задачная (task-based) система

Пользователь выполняет задания → получает allocation. Типичные задания:

  • Follow в Twitter, Discord, Telegram
  • Testnet транзакции
  • Referral новых пользователей
  • Участие в governance голосовании

Проблема: эти задания легко фармятся ботами. Задания должны требовать on-chain активности, которую сложно симулировать в масштабе.

Интеграция с Galxe / Layer3 — готовые платформы для task-based кампаний. API для верификации on-chain задач. Минус: платформа берёт fee и пользователи остаются на платформе, а не на твоём сайте.

Vested airdrop

Полученные токены не клеймятся сразу, а вестируются. Linear vesting 6–12 месяцев.

contract VestedAirdrop is MerkleDistributor {
    uint256 public immutable vestingStart;
    uint256 public immutable vestingDuration;

    mapping(address => uint256) public claimed;
    mapping(address => uint256) public totalAllocated;

    function claimVested(
        uint256 index,
        address account,
        uint256 totalAmount,
        bytes32[] calldata merkleProof
    ) external {
        // Верификация аллокации (если первый claim)
        if (totalAllocated[account] == 0) {
            _verifyAndSetAllocation(index, account, totalAmount, merkleProof);
        }

        uint256 vested = _vestedAmount(account);
        uint256 claimable = vested - claimed[account];
        require(claimable > 0, "Nothing to claim");

        claimed[account] += claimable;
        IERC20(token).safeTransfer(account, claimable);
        emit VestedClaimed(account, claimable);
    }

    function _vestedAmount(address account) internal view returns (uint256) {
        if (block.timestamp < vestingStart) return 0;
        uint256 elapsed = block.timestamp - vestingStart;
        if (elapsed >= vestingDuration) return totalAllocated[account];
        return totalAllocated[account] * elapsed / vestingDuration;
    }
}

Cliff + linear: первые 3 месяца ничего (cliff), затем линейный вест 9 месяцев. Снижает immediate dump, создаёт долгосрочных holders.

Система начисления баллов

Для сложных кампаний с множеством действий — off-chain система баллов:

interface UserScore {
  address: string;
  totalPoints: number;
  breakdown: {
    earlyAdopter: number;        // первые 1000 пользователей
    volumeScore: number;         // на основе торгового объёма
    loyaltyScore: number;        // длительность использования
    referrals: number;           // успешные рефералы
    governanceVotes: number;     // участие в голосованиях
  };
}

// Allocation = f(points) с diminishing returns для анти-whale механизма
function calculateAllocation(points: number, totalPoints: number): bigint {
  // Квадратный корень для снижения whale доминирования
  const sqrtScore = Math.sqrt(points);
  const totalSqrtScore = /* sum of sqrt scores for all users */ 0;
  const allocation = (TOTAL_AIRDROP_AMOUNT * BigInt(Math.floor(sqrtScore * 1e18)))
    / BigInt(Math.floor(totalSqrtScore * 1e18));
  return allocation;
}

Square root formula (используется в quadratic voting и некоторых airdrop системах): уменьшает разрыв между крупными и мелкими участниками. Whale с 10,000 points получит не в 10x больше чем пользователь с 1,000 points, а только в ~3.16x.

Gas optimization для mass claiming

При миллионах claimer-ов каждый сэкономленный gas — это деньги пользователей:

EIP-2612 Permit — вместо отдельной approve транзакции, если пользователю нужно что-то сделать с токенами сразу после claim (например, застейкать):

function claimAndStake(
    uint256 index,
    uint256 amount,
    bytes32[] calldata proof,
    uint256 deadline,
    uint8 v, bytes32 r, bytes32 s
) external {
    // Claim токены
    claim(index, msg.sender, amount, proof);

    // Permit для approve без отдельной транзакции
    IERC20Permit(token).permit(msg.sender, address(staking), amount, deadline, v, r, s);

    // Стейкаем сразу
    staking.stakeFor(msg.sender, amount);
}

Batch claiming — если один пользователь имеет allocations в нескольких раундах:

function claimMultiple(
    uint256[] calldata indices,
    uint256[] calldata amounts,
    bytes32[][] calldata proofs
) external {
    uint256 totalAmount;
    for (uint i = 0; i < indices.length; i++) {
        // верификация каждого proof
        totalAmount += amounts[i];
    }
    // один transfer вместо N
    IERC20(token).safeTransfer(msg.sender, totalAmount);
}

Frontend для airdrop

Eligibility checker — ввод адреса → проверка через API (backend имеет список) или прямо из Merkle tree (если опубликован полностью):

async function checkEligibility(address: string) {
  // Нормализация адреса
  const normalizedAddress = ethers.getAddress(address);

  // Получаем данные из API или из opubликованного snapshot
  const allocation = await fetchAllocation(normalizedAddress);

  if (!allocation) {
    return { eligible: false, amount: 0n, proof: [] };
  }

  const proof = getMerkleProof(merkleTree, allocation.index, normalizedAddress, allocation.amount);

  // Проверяем не клеймил ли уже
  const alreadyClaimed = await distributor.isClaimed(allocation.index);

  return {
    eligible: true,
    amount: allocation.amount,
    proof,
    alreadyClaimed
  };
}

Snapshot публикация: Merkle tree данные должны быть публично доступны (GitHub, IPFS) чтобы пользователи могли независимо верифицировать свою аллокацию. Непубличный snapshot — красный флаг для community.

Expiry и unclaimed tokens

Всегда устанавливать expiry на claim период (обычно 1 год). Unclaimed токены возвращаются в treasury или сжигаются:

uint256 public constant EXPIRY = 365 days;
uint256 public immutable deployedAt;

function recoverUnclaimed() external onlyOwner {
    require(block.timestamp > deployedAt + EXPIRY, "Not expired");
    uint256 remaining = IERC20(token).balanceOf(address(this));
    IERC20(token).safeTransfer(treasury, remaining);
}