Розробка системи масового розповсюження токенів
Масова роздача токенів — це не просто цикл з transfer() у смарт-контракті. Коли отримувачів десятки тисяч, а кожна транзакція коштує газ, неправильна архітектура перетворює airdrop на збиткове підприємство для проекту та кошмар для користувачів, які не можуть отримати токени годинами через перевантаження мережі.
Завдання системи масового розповсюження: доставити токени максимальній кількості адрес з мінімальними витратами на газ, із захистом від зловживань та можливістю аудиту кожної виплати.
Push vs Pull: ключовий архітектурний вибір
Перше рішення — хто ініціює передачу токенів.
Push (проект → користувач): проект самостійно відправляє токени на адреси. Просто для користувача, дорого для проекту. При 50,000 отримувачів та стандартному transfer (21,000 gas) на Ethereum mainnet — мільйони доларів газу при будь-якому навантаженні на мережу.
Pull (користувач забирає): отримувач самостійно вызиває claim(). Витрати на газ падають на користувача. Додатковий бар'єр входу, але економічно справедливіше.
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: користувач підписує своїм кошельком повідомлення, що підтверджує власність.
Tiered airdrop: різні суми для різних категорій активності. Ранні користувачі отримують більше, ніж ті, що почали за тиждень до 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 контракті) з заданим розписанням. Дорожче по газу, але дає повну гнучкість у налаштуванні індивідуальних vesting schedule.
Мониторинг та аналітика
Після запуску airdrop слід відслідковувати: відсоток claimed від загального пулу, скільки адрес claim'нули у перші 24/48/72 годин, адреси, що негайно продають (on-chain через DEX events), середнє утримання токенів через 30 днів.
Ці метрики прямо пов'язані з якістю аудиторії та дозволяють скорегулювати наступний airdrop.
Терміни розробки
Merkle distributor з off-chain генерацією дерева та базовим frontend для claim — 3–4 тижні. Повнофункціональна система з sybil-захистом, tiered розповсюженням, vesting інтеграцією та аналітичним dashboard — 2–3 місяці.







