Разработка системы массового распределения токенов
Массовая раздача токенов — это не просто цикл с transfer() в смарт-контракте. Когда получателей десятки тысяч, а каждая транзакция стоит gas, неправильная архитектура превращает airdrop в убыточное мероприятие для проекта и в кошмар для пользователей, которые не могут получить токены часами из-за перегрузки сети.
Задача системы массового распределения: доставить токены максимальному числу адресов с минимальными затратами на gas, с защитой от злоупотреблений и с возможностью аудита каждой выплаты.
Push vs Pull: ключевой архитектурный выбор
Первое решение — кто инициирует передачу токенов.
Push (project → user): проект сам отправляет токены на адреса. Просто для пользователя, дорого для проекта. При 50,000 получателях и стандартном transfer (21,000 gas) на Ethereum mainnet — миллионы долларов gas при любой нагрузке на сеть.
Pull (user claims): получатель сам вызывает claim(). Затраты на gas ложатся на пользователя. Дополнительный барьер входа, но экономически более честно.
Merkle drop (pull с доказательством): золотой стандарт для крупных airdrop. Список получателей публикуется как Merkle tree, on-chain хранится только root. Пользователь предоставляет proof принадлежности своего адреса к списку.
contract MerkleDistributor {
address public immutable token;
bytes32 public immutable merkleRoot;
// Битовая карта для отслеживания claim без хранения маппинга адресов
mapping(uint256 => uint256) private claimedBitMap;
event Claimed(uint256 indexed index, address indexed account, uint256 amount);
constructor(address token_, bytes32 merkleRoot_) {
token = token_;
merkleRoot = merkleRoot_;
}
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 claim(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external {
require(!isClaimed(index), "Already claimed");
bytes32 node = keccak256(abi.encodePacked(index, account, amount));
require(
MerkleProof.verify(merkleProof, merkleRoot, node),
"Invalid proof"
);
_setClaimed(index);
IERC20(token).transfer(account, amount);
emit Claimed(index, account, amount);
}
function _setClaimed(uint256 index) private {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[claimedWordIndex] |= (1 << claimedBitIndex);
}
}
Битовая карта вместо mapping(address => bool) экономит существенный объём storage, особенно при сотнях тысяч получателей.
Генерация и верификация Merkle tree
Off-chain генерация дерева — критически важный этап. Ошибка в списке получателей означает невозможность получить токены или двойные выплаты.
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');
const { ethers } = require('ethers');
function generateMerkleTree(recipients) {
// recipients: [{ index, address, amount }, ...]
const leaves = recipients.map(({ index, address, amount }) => {
return ethers.solidityPackedKeccak256(
['uint256', 'address', 'uint256'],
[index, address, amount]
);
});
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const root = tree.getHexRoot();
// Генерируем proofs для каждого получателя
const proofs = recipients.map(({ index, address, amount }, i) => ({
index,
address,
amount: amount.toString(),
proof: tree.getHexProof(leaves[i])
}));
return { root, proofs };
}
// Публикуем proofs через IPFS или API
// Пользователь получает свой proof и вызывает claim()
Защита от sybil и злоупотреблений
Массовая раздача без защиты — это приглашение к sybil-атаке. Типичный сценарий: атакующий создаёт тысячи кошельков, выполняет минимальные требования с каждого и получает непропорциональную долю airdrop.
Уровни защиты:
On-chain критерии: учитывать только кошельки с реальной историей — первая транзакция до определённой даты, объём комиссий выше порога, взаимодействие с конкретными протоколами.
Off-chain верификация: Gitcoin Passport (агрегатор identity proof), Proof of Humanity, верификация через social proof (Twitter/Github аккаунт связанный с адресом). Интегрируется через signature: пользователь подписывает своим кошельком сообщение, подтверждающее ownership.
Tiered airdrop: разные суммы для разных категорий активности. Early users получают больше, чем те, кто начал взаимодействовать за неделю до snapshot.
| Категория | Критерий | Множитель |
|---|---|---|
| OG users | Первая транзакция > 12 месяцев назад | 3x |
| Active users | > 10 транзакций за последние 6 месяцев | 2x |
| Regular users | Хотя бы 1 транзакция за последние 3 месяца | 1x |
| Snapshot hunters | Транзакция за последние 2 недели | 0.5x |
Batch distribution для push-сценариев
Когда push необходим (например, компенсация пострадавшим в exploit, retroactive rewards для верифицированных адресов), используют batch transfer контракты.
Простой паттерн: multicall с несколькими transfer() в одной транзакции. Более эффективный: Disperse.app паттерн — один вызов контракта, который итерируется по массиву получателей.
function disperseToken(
IERC20 token,
address[] calldata recipients,
uint256[] calldata amounts
) external {
uint256 total = 0;
for (uint256 i = 0; i < amounts.length; i++) {
total += amounts[i];
}
token.transferFrom(msg.sender, address(this), total);
for (uint256 i = 0; i < recipients.length; i++) {
token.transfer(recipients[i], amounts[i]);
}
}
Оптимальный размер батча — 200–500 адресов на транзакцию в зависимости от сети. Превышение лимита block gas limit приводит к reverts.
Vesting поверх airdrop
Для команд и инвесторов часто комбинируют массовое распределение с vesting: токены клеймятся, но не сразу доступны полностью, а разблокируются по расписанию.
Реализация: при claim пользователь не получает токены напрямую, а деплоится персональный VestingWallet (или создаётся запись в общем vesting контракте) с заданным расписанием. Это дороже по gas, но даёт полную гибкость в настройке индивидуальных vesting schedules.
Мониторинг и аналитика
После запуска airdrop нужно отслеживать: процент claimed от общего пула, сколько адресов claim'нули в первые 24/48/72 часа, адреса которые немедленно продают (on-chain через DEX events), средний retention токенов через 30 дней.
Эти метрики напрямую связаны с качеством аудитории и позволяют скорректировать следующий airdrop.
Сроки разработки
Merkle distributor с off-chain генерацией дерева и базовым frontend для claim — 3–4 недели. Полноценная система с sybil-защитой, tiered распределением, vesting интеграцией и аналитическим dashboard — 2–3 месяца.







