Розробка Sybil-resistant airdrop системи
Sybil-атака на airdrop — не екзотика, це норма. Будь-який airdrop, оголошений заздалегідь, отримує армію ботів ще до момента snapshot. Після того як Optimism роздав токени в 2022 році і швидко виявилося, що значна частина отримувачів — кластери Sybil-гаманців, стандарт для серйозних дистрибуцій змінився. Тепер Sybil-resistance — обов'язковий компонент, а не опція.
Анатомія Sybil-атаки на airdrop
Атакуючий створює N гаманців, імітує критерії eligibility на кожному, отримує N долей airdrop, переводить все на головний гамань. Складність атаки залежить від вартості імітації одного гамання versus очікуваної виплати.
Три рівні Sybil-атак за складністю:
Рівень 1 — address farming. Створити гаманці, зробити мінімальні дії. Дёшево і масово. Працює проти простих критеріїв типу «хоча б одна транзакція в мережі».
Рівень 2 — activity simulation. Скриптовані взаємодії з протоколами: свапи, bridging, liquidity provision. Вартість вище — gas + час. Працює проти критеріїв обсягу транзакцій.
Рівень 3 — social graph manipulation. Створення зв'язків між акаунтами, імітація organic behavior. Дорого, але застосовується при великих сумах airdrop.
Ключове спостереження: жодний метод не дає 100% захисту. Ціль — піднести вартість Sybil-атаки вище очікуваного прибутку.
On-chain аналіз: кластеризація адрес
Граф транзакцій та common funder detection
Найбазовіший і найефективніший метод — аналіз джерела фінансування гаманців. Sybil-гаманці майже завжди фінансуються з однієї адреси або через один CEX-вивід.
import networkx as nx
from collections import defaultdict
def build_funding_graph(addresses: list[str], provider) -> nx.DiGraph:
"""
Будуємо граф: хто кого фінансував.
addresses — список eligible адрес.
"""
G = nx.DiGraph()
for addr in addresses:
# Беремо першу вхідну транзакцію (initial funding)
first_tx = provider.get_first_incoming_tx(addr)
if first_tx:
funder = first_tx['from']
G.add_edge(funder, addr, tx=first_tx['hash'])
return G
def detect_sybil_clusters(G: nx.DiGraph, threshold: int = 5) -> list[list[str]]:
"""
Якщо одна адреса профінансувала >= threshold eligible адрес — кластер.
"""
clusters = []
for node in G.nodes():
successors = list(G.successors(node))
if len(successors) >= threshold:
clusters.append(successors)
return clusters
На практиці threshold підбирається під конкретний airdrop. Для великого airdrop з тисячами адрес — 5 цілком консервативно. Для маленького — достатньо 3.
Temporal pattern analysis
Sybil-гаманці створюються та використовуються батчами. Характерний паттерн: N гаманців активні в однакові часові вікна, з однаковими паузами між транзакціями.
def detect_temporal_clusters(
addresses: list[str],
txs_by_address: dict,
time_window_seconds: int = 300,
min_cluster_size: int = 3
) -> list[list[str]]:
"""
Шукаємо адреси, чиї транзакції попадають в однакові
часові вікна підозріло часто.
"""
# Групуємо транзакції по часовим слотам
time_slots = defaultdict(list)
for addr, txs in txs_by_address.items():
for tx in txs:
slot = tx['timestamp'] // time_window_seconds
time_slots[slot].append(addr)
# Будуємо граф спільної активності
co_activity = defaultdict(lambda: defaultdict(int))
for slot, addrs in time_slots.items():
for i, a in enumerate(addrs):
for b in addrs[i+1:]:
co_activity[a][b] += 1
co_activity[b][a] += 1
# Кластери з високою спільною активністю
clusters = []
visited = set()
for addr in addresses:
if addr in visited:
continue
cluster = [addr]
for other, count in co_activity[addr].items():
if count >= 3 and other not in visited: # збігалися 3+ рази
cluster.append(other)
if len(cluster) >= min_cluster_size:
clusters.append(cluster)
visited.update(cluster)
return clusters
Gas price та nonce patterns
Скрипти, що створюють Sybil-гаманці, часто використовують однакові параметри. Однакові gasPrice / maxFeePerGas в транзакціях різних гаманців в одному блоці — сильний сигнал. Nonce = 0 або 1 у великої кількості адрес говорить про недавнє створення під конкретний airdrop.
Identity-based верифікація
Proof of Humanity та Worldcoin
Найнадійніший Sybil-resistance — доказ унікальності особистості. Два основних протоколи:
Proof of Humanity (PoH): відео-реєстрація + соціальна верифікація (vouch від існуючих учасників). Зберігає список verified humans on-chain (Gnosis Chain). Повільно масштабується, але висока якість.
Worldcoin: біометрична верифікація через iris scan на Orb-пристроях. Видає World ID — ZK-proof унікальності особистості без розкриття біометрії.
interface IWorldID {
function verifyProof(
uint256 root,
uint256 groupId,
uint256 signalHash,
uint256 nullifierHash,
uint256 externalNullifierHash,
uint256[8] calldata proof
) external view;
}
contract SybilResistantAirdrop {
IWorldID public immutable worldId;
uint256 public immutable groupId = 1;
uint256 public immutable externalNullifier;
// nullifierHash привязаний до конкретної дії (claim цього airdrop)
// один человек = один nullifierHash = один claim
mapping(uint256 => bool) public usedNullifiers;
constructor(IWorldID _worldId, string memory appId, string memory action) {
worldId = _worldId;
externalNullifier = abi.encodePacked(
abi.encodePacked(appId).hashToField(),
abi.encodePacked(action).hashToField()
).hashToField();
}
function claim(
address recipient,
uint256 root,
uint256 nullifierHash,
uint256[8] calldata proof
) external {
require(!usedNullifiers[nullifierHash], "Already claimed");
worldId.verifyProof(
root,
groupId,
abi.encodePacked(recipient).hashToField(),
nullifierHash,
externalNullifier,
proof
);
usedNullifiers[nullifierHash] = true;
_distributeTokens(recipient);
}
}
Ключова властивість: nullifierHash унікальний для комбінації (людина, дія). Один человек не може отримати два різні nullifierHash-і для однієї дії — це забезпечується ZK-proof-ом.
Gitcoin Passport
Gitcoin Passport агрегує сигнали з різних джерел: GitHub акаунт, Twitter, ENS імя, BrightID верифікація, Proof of Humanity, POAP колекції. Кожен stamp дає score. Вище score — вище вероятність що це реальна людина.
// Перевірка Gitcoin Passport score через API
async function getPassportScore(address: string): Promise<number> {
const response = await fetch(
`https://api.scorer.gitcoin.co/registry/score/${scorerId}/${address}`,
{ headers: { 'X-API-KEY': process.env.GITCOIN_API_KEY! } }
);
const data = await response.json();
return parseFloat(data.score ?? '0');
}
// Поріг score для eligibility
const MINIMUM_PASSPORT_SCORE = 15.0;
async function buildEligibleList(candidates: string[]): Promise<string[]> {
const scores = await Promise.all(
candidates.map(async (addr) => ({
address: addr,
score: await getPassportScore(addr)
}))
);
return scores
.filter(({ score }) => score >= MINIMUM_PASSPORT_SCORE)
.map(({ address }) => address);
}
Багаторівнева scoring система
На практиці жоден метод не використовується в ізоляції. Найкращий підхід — scoring: кожна адреса отримує баллы за різні сигнали, фінальна eligibility визначається сумою.
| Критерій | Баллы | Обґрунтування |
|---|---|---|
| World ID верифікація | +50 | Унікальність особистості |
| Gitcoin Passport ≥ 20 | +30 | Агрегована ідентичність |
| ENS імя | +10 | Довгострокова on-chain identity |
| Вік гамання > 1 року | +10 | Не створено під airdrop |
| Активність в кількох протоколах | +15 | Organic usage pattern |
| Без збігів в фандинг-графі | +5 | Не частина Sybil-кластера |
| Сума для eligibility | ≥ 40 | Налаштовується під проект |
Адреси, що потрапили в Sybil-кластер при аналізі графа, отримують penalty або повну дисквалифікацію незалежно від score.
Апеляційний механізм
Будь-яка автоматична фільтрація дає false positives. Потрібен процес обжалування:
- On-chain appeal: адреса відправляє транзакцію з доказом (Proof of Humanity proof, підпис від PoH реєстрації, пояснення пов'язаності гаманців)
- Timelock: період для подачі апелій фіксований (наприклад, 14 днів після публікації snapshot)
- Multisig review: команда розглядає апелії через multisig, рішення прозорі on-chain
- Публічний список: всі відфільтровані адреси і причини публікуються до початку claim-періоду
Прозорість критична — спільнота повинна бачити, що фільтрація не произвільна.
Архітектура контракту дистрибуції
contract SybilResistantDistributor {
bytes32 public merkleRoot;
mapping(address => bool) public claimed;
// Адреси, заблоковані за результатами Sybil-аналізу
mapping(address => bool) public sybilBlacklist;
// Мінімальний Passport score (зберігається off-chain, верифікується підписом)
address public scoreOracle;
struct ClaimData {
uint256 amount;
bytes32[] merkleProof;
// Опціонально: підпис оракула що підтверджує Passport score
bytes oracleSignature;
uint256 passportScore;
}
function claim(ClaimData calldata data) external nonReentrant {
require(!claimed[msg.sender], "Already claimed");
require(!sybilBlacklist[msg.sender], "Address flagged as Sybil");
// Верифікуємо Passport score якщо потрібно
if (requirePassport) {
_verifyPassportScore(msg.sender, data.passportScore, data.oracleSignature);
require(data.passportScore >= minPassportScore, "Insufficient Passport score");
}
// Верифікуємо merkle proof
bytes32 leaf = keccak256(bytes.concat(
keccak256(abi.encode(msg.sender, data.amount))
));
require(
MerkleProof.verify(data.merkleProof, merkleRoot, leaf),
"Invalid proof"
);
claimed[msg.sender] = true;
token.safeTransfer(msg.sender, data.amount);
emit Claimed(msg.sender, data.amount);
}
function _verifyPassportScore(
address user,
uint256 score,
bytes calldata sig
) internal view {
bytes32 hash = keccak256(abi.encodePacked(user, score, block.chainid));
bytes32 ethHash = hash.toEthSignedMessageHash();
require(ethHash.recover(sig) == scoreOracle, "Invalid oracle signature");
}
}
Часові затримки як захист
Один з недооцінених інструментів — часове вікно між snapshot та оголошенням eligibility. Якщо airdrop оголошений без дати snapshot, farming вже йде. Найкраща практика:
- Snapshot робиться без попередження або з мінімальним сповіщенням
- Eligibility оголошується після snapshot — у атакуючих немає часу адаптуватися
- Merkle root деплоїться через timelock (24–72 години) — спільнота може перевірити список до початку claim
Інструменти та дані
Аналіз on-chain даних: Dune Analytics (готові запити за Sybil паттернам), Nansen (wallet labeling), Arkham Intelligence (entity clustering).
Identity протоколи: Worldcoin (World ID), Proof of Humanity, BrightID, Gitcoin Passport, Lens Protocol (social graph).
Технічні інструменти: Python + networkx для граф-аналізу, The Graph для індексації, Alchemy/QuickNode для bulk RPC запитів.
Строки та scope робіт
| Фаза | Содержание | Срок |
|---|---|---|
| Дизайн критеріїв eligibility | Визначення метрик, порогів, scoring моделі | 1–2 тиж |
| On-chain аналіз | Збір даних, кластеризація, фільтрація | 2–3 тиж |
| Смарт-контракти | Distributor + identity інтеграція | 2–3 тиж |
| Off-chain інфраструктура | API, Merkle tree, оракул підписів | 2–3 тиж |
| Апеляційний процес | Контракт + UI + процедура | 1–2 тиж |
| Аудит та тестування | 2–3 тиж |
Мінімальний реалістичний срок для production-ready системи — 10–16 тижнів. Поспіх у Sybil-фільтрації дорого обходиться репутаційно: якщо спільнота бачить очевидних ботів у списку отримувачів, довіра до проекту падає різко.







