Разработка 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-кошельки, часто используют одинаковые параметры gas. Одинаковый 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 уникален для комбинации (человек, action). Один человек не может получить два разных nullifierHash для одного action — это обеспечивается ZK-proof-ом.
Gitcoin Passport
Gitcoin Passport агрегирует signals из разных источников: 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. Если аirdrop анонсирован без 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-фильтрации дорого обходится репутационно: если сообщество видит очевидных ботов в списке получателей, доверие к проекту падает резко.







