Розробка системи голосування для держателів fan-токенів
Fan-токени — це особлива категорія токенів управління. Chiliz, Socios, Rally — ринки, де спортивні клуби та діячі культури видають токени й дають емітентам право голосу в рішеннях: дизайн комплекту, пісні на стадіоні, меню фанзони. Це механіка залучення, не фінансове управління — архітектура принципово відрізняється від протоколів DAO.
Основні вимоги: висока пропускна здатність (тисячі голосів за години), простий UX (фанати не крипто-нативні), захист від маніпуляцій (один кит не повинен перекривати спільноту), прозорість результатів.
Архітектура: On-chain vs Off-chain vs Гібрид
Чистий on-chain не працює: витрати газу відпугують масову аудиторію; затримка мережі створює затримки; опитування типу "який колір для нових кросівок" не потребують блокчейна для кожного голосу.
Рекомендується: гібридний підхід:
- Снапшот балансу токенів taken on-chain (конкретний блок = конкретний момент)
- Самі голоси — off-chain підписи (як Snapshot Protocol)
- Агреговані результати та доказ коректного підрахунку опубліковані on-chain
contract FanTokenVoting {
struct Poll {
uint256 id;
string title;
string[] choices;
uint256 snapshotBlock; // блок для снапшоту балансу
uint256 startTime;
uint256 endTime;
uint256 minTokenBalance; // мінімальний баланс для участі
PollStatus status;
bytes32 resultsHash; // хеш результатів після закриття
}
enum PollStatus { Draft, Active, Closed, ResultsPublished }
IERC20 public immutable fanToken;
address public operator; // клуб/команда
mapping(uint256 => Poll) public polls;
mapping(uint256 => mapping(uint256 => uint256)) public pollResults;
uint256 public pollCount;
event PollCreated(uint256 indexed pollId, string title, uint256 snapshotBlock);
event ResultsPublished(uint256 indexed pollId, bytes32 resultsHash, uint256[] voteCounts);
function createPoll(
string calldata title,
string[] calldata choices,
uint256 durationSeconds,
uint256 minTokenBalance
) external onlyOperator returns (uint256 pollId) {
require(choices.length >= 2 && choices.length <= 10, "Invalid choices");
pollId = ++pollCount;
polls[pollId] = Poll({
id: pollId,
title: title,
choices: choices,
snapshotBlock: block.number,
startTime: block.timestamp,
endTime: block.timestamp + durationSeconds,
minTokenBalance: minTokenBalance,
status: PollStatus.Active,
resultsHash: bytes32(0)
});
emit PollCreated(pollId, title, block.number);
}
}
Off-chain сервіс агрегації голосів
Off-chain сервіс перевіряє кожну підпис голосу, перевіряє баланс снапшоту й агрегує результати.
class VoteAggregator {
constructor(provider, fanTokenAddress) {
this.provider = provider;
this.fanToken = new ethers.Contract(
fanTokenAddress,
['function balanceOf(address) view returns (uint256)'],
provider
);
}
async processVote(vote, poll) {
// 1. Перевірити підпис
const messageHash = ethers.solidityPackedKeccak256(
['uint256', 'uint256', 'address'],
[vote.pollId, vote.choiceIndex, vote.voter]
);
const recoveredAddress = ethers.verifyMessage(
ethers.getBytes(messageHash),
vote.signature
);
if (recoveredAddress.toLowerCase() !== vote.voter.toLowerCase()) {
throw new Error('Invalid signature');
}
// 2. Перевірити баланс на блоці снапшоту
const balance = await this.fanToken.balanceOf(
vote.voter,
{ blockTag: poll.snapshotBlock }
);
if (balance < poll.minTokenBalance) {
throw new Error('Insufficient balance');
}
// 3. Перевірити часові рамки
if (vote.timestamp < poll.startTime || vote.timestamp > poll.endTime) {
throw new Error('Vote outside poll period');
}
return {
voter: vote.voter,
choice: vote.choiceIndex,
votingPower: balance
};
}
}
Захист від китів
Коли один емітент має 30–40% пропозиції, стандартне "1 токен = 1 голос" стає диктатурою. Один спекулятивний кит перекриває тисячі реальних фанатів.
Квадратичне голосування: голосувальна сила = √(баланс). 10,000 токенів дають вагу 100, не 10,000. Ефективно зменшує вплив китів.
Кеппінг: максимальна вага голосу обмежена (наприклад, 1% від загальних голосів). Просто, але грубо.
Часово-зважений баланс: рахує середній баланс за 30 днів, не поточний. Запобігає купівлі токенів перед голосуванням.
| Механізм | Складність | Захист від китів | Складність UX |
|---|---|---|---|
| 1 токен = 1 голос | Низька | Немає | Немає |
| Квадратичне голосування | Середня | Висока | Середня |
| Кеппінг балансу | Низька | Середня | Немає |
| Часово-зважено | Середня | Середня | Немає |
UX для масової аудиторії
Фанати не розуміють Web3. Пріоритет простоти:
Безгазове голосування: голоси через підписи без оплати газу. Оператор покриває через мета-транзакції (ERC-2771, Biconomy).
Соціальний логін: Magic.link або Privy — гаманець авто-створений на Google/Apple логіну. Фанат не знає, що у них есть гаманець.
Сповіщення: push-сповіщення про нові опитування та результати через Firebase + мобільний додаток. Snapshot.org як резервна для крипто-нативних користувачів.
Прозорість без складності: результати показуються як простий діаграма. On-chain верифікаційне посилання доступне, але не видиме.
Графік розробки
Бекенд (агрегація голосів, API) + смарт контракт + інтеграція: 6–8 тижнів. Повна платформа з мобільним додатком, соціальним логіном, сповіщеннями, аналітикою: 4–5 місяців.







