Разработка системы голосования для держателей fan-токенов

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка системы голосования для держателей fan-токенов
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Разработка системы голосования для держателей fan-токенов

Fan-токены — это особая категория governance-токенов. Chiliz, Socios, Rally — рынок, где спортивные клубы и деятели культуры выпускают токены и дают держателям право голоса в решениях: дизайн комплекта, выбор песни на стадионе, меню в фанзоне. Это не финансовый governance, а engagement механика — и архитектура системы голосования под неё принципиально отличается от DAO протоколов.

Основные требования: высокая throughput (тысячи голосов за часы), простой UX (фанаты — не крипто-нативная аудитория), защита от манипуляций (whale один не должен перекрывать community), прозрачность результатов.

Архитектура: on-chain vs off-chain vs hybrid

Для fan-токен голосований чистый on-chain подход работает плохо: gas costs отпугивают массовую аудиторию, латентность сети создаёт задержки, а опросы вида «какой цвет выбрать для новых кед» не требуют блокчейна для хранения каждого голоса.

Рекомендуемая схема — hybrid:

  • Snapshot баланса токенов берётся on-chain (конкретный блок = конкретный момент)
  • Сами голоса — off-chain подписи (как в Snapshot Protocol)
  • Агрегированный результат и доказательство корректного подсчёта публикуются on-chain
contract FanTokenVoting {
    struct Poll {
        uint256 id;
        string title;
        string[] choices;
        uint256 snapshotBlock;      // блок для snapshot балансов
        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; // pollId => choiceIndex => votes
    uint256 public pollCount;
    
    event PollCreated(uint256 indexed pollId, string title, uint256 snapshotBlock);
    event ResultsPublished(uint256 indexed pollId, bytes32 resultsHash, uint256[] voteCounts);
    
    modifier onlyOperator() {
        require(msg.sender == operator, "Not operator");
        _;
    }
    
    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 count");
        
        pollId = ++pollCount;
        polls[pollId] = Poll({
            id: pollId,
            title: title,
            choices: choices,
            snapshotBlock: block.number,    // snapshot прямо сейчас
            startTime: block.timestamp,
            endTime: block.timestamp + durationSeconds,
            minTokenBalance: minTokenBalance,
            status: PollStatus.Active,
            resultsHash: bytes32(0)
        });
        
        emit PollCreated(pollId, title, block.number);
    }
    
    function publishResults(
        uint256 pollId,
        uint256[] calldata voteCounts,
        bytes32 resultsHash
    ) external onlyOperator {
        Poll storage poll = polls[pollId];
        require(block.timestamp > poll.endTime, "Poll still active");
        require(poll.status == PollStatus.Closed || poll.status == PollStatus.Active, "Wrong status");
        
        for (uint256 i = 0; i < voteCounts.length; i++) {
            pollResults[pollId][i] = voteCounts[i];
        }
        
        poll.resultsHash = resultsHash;
        poll.status = PollStatus.ResultsPublished;
        
        emit ResultsPublished(pollId, resultsHash, voteCounts);
    }
    
    // Верификация: off-chain сервис агрегирует голоса и публикует хеш
    // resultsHash = keccak256(abi.encodePacked(pollId, voteCounts, allSignatures))
}

Off-chain сервис обработки голосов

Голоса приходят как подписанные сообщения. Сервис агрегации верифицирует каждую подпись, сверяет баланс на snapshot-блоке и агрегирует результаты.

const { ethers } = require('ethers');

class VoteAggregator {
    constructor(provider, fanTokenAddress) {
        this.provider = provider;
        this.fanToken = new ethers.Contract(
            fanTokenAddress,
            ['function balanceOf(address) view returns (uint256)'],
            provider
        );
    }
    
    // Структура vote: { pollId, choiceIndex, voter, signature }
    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. Проверяем баланс на snapshot блоке
        const balance = await this.fanToken.balanceOf(
            vote.voter,
            { blockTag: poll.snapshotBlock }
        );
        
        if (balance < poll.minTokenBalance) {
            throw new Error('Insufficient balance at snapshot');
        }
        
        // 3. Проверяем временные рамки
        const voteTimestamp = vote.timestamp;
        if (voteTimestamp < poll.startTime || voteTimestamp > poll.endTime) {
            throw new Error('Vote outside poll period');
        }
        
        return {
            voter: vote.voter,
            choice: vote.choiceIndex,
            votingPower: balance  // или нормализованный вес
        };
    }
    
    async aggregateResults(pollId, votes, poll) {
        const processedVoters = new Set();
        const choicePowers = new Array(poll.choices.length).fill(0n);
        
        for (const vote of votes) {
            if (processedVoters.has(vote.voter.toLowerCase())) {
                continue; // Берём только последний голос пользователя
            }
            
            try {
                const processed = await this.processVote(vote, poll);
                choicePowers[processed.choice] += processed.votingPower;
                processedVoters.add(vote.voter.toLowerCase());
            } catch (e) {
                console.warn(`Invalid vote from ${vote.voter}: ${e.message}`);
            }
        }
        
        return choicePowers;
    }
}

Защита от whale-доминирования

Когда один держатель имеет 30–40% supply, стандартное голосование «1 токен = 1 голос» превращается в диктатуру. Для fan-токенов это особенно болезненно: один крупный спекулятивный держатель перекрывает тысячи реальных фанатов.

Квадратичное голосование (Quadratic Voting): вес голоса = √(баланс токенов). 10,000 токенов дают голосовой вес 100, а не 10,000. Эффективно снижает whale power.

function calculateVotingPower(balance) {
    // Квадратный корень из баланса (нормализованного)
    // balance в наименьших единицах, нормализуем к "целым токенам"
    const normalizedBalance = Number(balance / 10n**18n);
    return Math.sqrt(normalizedBalance);
}

Capping: максимальный вес голоса ограничен, например, 1% от общего числа голосов в опросе. Прост в реализации, грубоват.

Time-weighted balance: учитывается не текущий баланс, а средний за последние 30 дней. Препятствует покупке токенов непосредственно перед голосованием.

Механизм Сложность Эффективность против whale Сложность для UX
1 токен = 1 голос Низкая Нет Нет
Quadratic voting Средняя Высокая Средняя
Balance capping Низкая Средняя Нет
Time-weighted Средняя Средняя Нет
Conviction voting Высокая Высокая Высокая

Особенности UX для массовой аудитории

Фанаты не разбираются в Web3. Необходим приоритет простоты:

Gasless voting: голоса через подписи (EIP-712) без оплаты gas. Транзакцию при необходимости оплачивает оператор через meta-transactions (ERC-2771, Biconomy).

Social login: интеграция с Magic.link или Privy — кошелёк создаётся автоматически при входе через Google/Apple. Фанат не знает, что у него есть кошелёк.

Уведомления: push-уведомления о новых опросах и результатах через Firebase + мобильное приложение. Snapshot.org как резервный интерфейс для крипто-нативных пользователей.

Прозрачность без сложности: результаты голосования отображаются как простая диаграмма. Ссылка на on-chain результаты доступна для тех, кто хочет верифицировать, но не выставляется на первый план.

Интеграция с реальными решениями клуба

Голосование должно иметь binding nature — иначе теряет смысл. Техническая интеграция: результаты автоматически публикуются через API клуба в официальные каналы, у каждого опроса есть явное описание «как именно будет реализовано решение».

Для критических решений (смена названия команды, дизайн стадиона) часто добавляют двухэтапный процесс: soft poll (Snapshot) → если порог участия достигнут → official poll с публикацией on-chain результатов и юридически обязывающим обязательством клуба.

Сроки разработки

Backend (vote aggregation service, API) + смарт-контракт + интеграция с fan-token: 6–8 недель. Полноценная платформа с мобильным приложением, social login, уведомлениями и аналитикой: 4–5 месяцев.