Разработка системы ретроактивного финансирования общественных благ

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка системы ретроактивного финансирования общественных благ
Сложная
~1-2 недели
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • 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

Разработка conviction voting (непрерывное голосование)

Стандартное snapshot-голосование в DAO страдает от нескольких проблем: voting period фиксированный (если пропустил — не проголосовал), whale может придержать голоса до последнего момента и перевернуть результат, каждый proposal — отдельное голосовательное событие с низкой явкой. Conviction voting решает эти проблемы через принципиально другую механику: голос накапливает «убеждённость» со временем, а proposal проходит когда накопленная убеждённость пересекает динамический threshold.

Gardens (1Hive), TE Commons, Giveth — протоколы, использующие conviction voting для управления treasury grants. Механику разработал Jeff Emmett, детально описал в Gardens whitepaper.

Математическая модель

Сердце системы — функция накопления убеждённости:

conviction(t) = conviction(t-1) * α + votes * (1 - α)

где:

  • α (alpha) — коэффициент затухания, 0 < α < 1. Типичное значение: 0.9 (медленное накопление) до 0.5 (быстрое)
  • votes — текущий вес голосов, поддерживающих proposal
  • conviction(t) — накопленная убеждённость в момент t

При α = 0.9: для достижения 100% conviction при постоянных votes нужно ~22 периода (дней, если период = 1 день). Это означает: ранние поддерживатели накапливают больше conviction, поздние манипуляции менее эффективны.

Порог прохождения также динамический, зависит от размера запроса относительно доступного treasury:

threshold = threshold_min + (total_supply² * max_ratio) / (requested_amount * (total_supply - staked_at_this_proposal)²)

Логика: чем больший процент treasury запрашивает proposal, тем выше порог. Это защищает от одобрения огромных transfers при малой явке.

Архитектура контракта

ConvictionVoting контракт

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

contract ConvictionVoting is ReentrancyGuard {
    IERC20 public immutable token;
    IERC20 public immutable requestToken; // токен treasury (USDC, WETH)
    address public immutable vault;        // treasury vault

    // Параметры модели (управляются governance)
    uint256 public constant PRECISION = 1e7;
    uint256 public alpha;          // коэффициент затухания * PRECISION
    uint256 public maxRatio;       // максимальный % treasury за один proposal
    uint256 public minThresholdStake; // минимальный % stake для любого proposal

    struct Proposal {
        uint256 requestedAmount;
        address beneficiary;
        uint256 stakedTokens;      // суммарные голоса за этот proposal
        uint256 convictionLast;    // накопленная убеждённость на последнем update
        uint256 blockLast;         // блок последнего обновления
        ProposalStatus status;
        string title;
        string link;               // IPFS ссылка на описание
    }

    enum ProposalStatus { Active, Passed, Rejected, Cancelled }

    mapping(uint256 => Proposal) public proposals;
    uint256 public proposalCount;

    // Распределение голосов пользователя: address => proposalId => amount
    mapping(address => mapping(uint256 => uint256)) public voterStake;
    // Суммарные голоса пользователя во всех proposals
    mapping(address => uint256) public totalVoterStake;

    event ProposalAdded(uint256 indexed proposalId, address indexed beneficiary, uint256 amount);
    event StakeAdded(uint256 indexed proposalId, address indexed voter, uint256 amount, uint256 conviction);
    event StakeWithdrawn(uint256 indexed proposalId, address indexed voter, uint256 amount);
    event ProposalExecuted(uint256 indexed proposalId, uint256 amount);

    constructor(
        address _token,
        address _requestToken,
        address _vault,
        uint256 _alpha,        // например, 9000000 (0.9 * PRECISION)
        uint256 _maxRatio,     // например, 200000 (20% * PRECISION)
        uint256 _minThreshold  // например, 20000 (2% * PRECISION)
    ) {
        token = IERC20(_token);
        requestToken = IERC20(_requestToken);
        vault = _vault;
        alpha = _alpha;
        maxRatio = _maxRatio;
        minThresholdStake = _minThreshold;
    }

    function addProposal(
        uint256 requestedAmount,
        address beneficiary,
        string calldata title,
        string calldata link
    ) external returns (uint256) {
        require(requestedAmount > 0, "Amount must be positive");
        require(beneficiary != address(0), "Invalid beneficiary");

        uint256 vaultBalance = requestToken.balanceOf(vault);
        require(
            requestedAmount <= vaultBalance * maxRatio / PRECISION,
            "Exceeds max ratio"
        );

        uint256 proposalId = ++proposalCount;
        proposals[proposalId] = Proposal({
            requestedAmount: requestedAmount,
            beneficiary: beneficiary,
            stakedTokens: 0,
            convictionLast: 0,
            blockLast: block.number,
            status: ProposalStatus.Active,
            title: title,
            link: link
        });

        emit ProposalAdded(proposalId, beneficiary, requestedAmount);
        return proposalId;
    }

    function stakeToProposal(uint256 proposalId, uint256 amount) external nonReentrant {
        Proposal storage proposal = proposals[proposalId];
        require(proposal.status == ProposalStatus.Active, "Not active");

        uint256 availableBalance = token.balanceOf(msg.sender) - totalVoterStake[msg.sender];
        require(amount <= availableBalance, "Insufficient unstaked tokens");

        _updateConviction(proposalId);

        voterStake[msg.sender][proposalId] += amount;
        totalVoterStake[msg.sender] += amount;
        proposal.stakedTokens += amount;

        emit StakeAdded(proposalId, msg.sender, amount, proposal.convictionLast);
    }

    function withdrawFromProposal(uint256 proposalId, uint256 amount) external nonReentrant {
        require(voterStake[msg.sender][proposalId] >= amount, "Insufficient stake");

        _updateConviction(proposalId);

        voterStake[msg.sender][proposalId] -= amount;
        totalVoterStake[msg.sender] -= amount;
        proposals[proposalId].stakedTokens -= amount;

        emit StakeWithdrawn(proposalId, msg.sender, amount);
    }

    function executeProposal(uint256 proposalId) external nonReentrant {
        Proposal storage proposal = proposals[proposalId];
        require(proposal.status == ProposalStatus.Active, "Not active");

        _updateConviction(proposalId);

        uint256 threshold = calculateThreshold(proposal.requestedAmount);
        require(proposal.convictionLast >= threshold, "Insufficient conviction");

        proposal.status = ProposalStatus.Passed;

        // Выплата из vault через approved allowance
        requestToken.transferFrom(vault, proposal.beneficiary, proposal.requestedAmount);

        emit ProposalExecuted(proposalId, proposal.requestedAmount);
    }
}

Функция обновления убеждённости

Это ключевая функция — вычисляет накопленную conviction с момента последнего обновления:

function _updateConviction(uint256 proposalId) internal {
    Proposal storage proposal = proposals[proposalId];
    uint256 blocksPassed = block.number - proposal.blockLast;

    if (blocksPassed == 0) return;

    // conviction(t) = conviction(t-1) * alpha^blocksPassed + stakedTokens * (1 - alpha^blocksPassed)
    // Вычисляем alpha^blocksPassed через быстрое возведение в степень
    uint256 alphaPow = _pow(alpha, blocksPassed);

    proposal.convictionLast =
        (proposal.convictionLast * alphaPow / PRECISION) +
        (proposal.stakedTokens * (PRECISION - alphaPow) / PRECISION);

    proposal.blockLast = block.number;
}

// Быстрое возведение в степень для fixed-point арифметики
function _pow(uint256 base, uint256 exponent) internal pure returns (uint256) {
    if (exponent == 0) return PRECISION;
    uint256 result = PRECISION;
    while (exponent > 0) {
        if (exponent % 2 == 1) {
            result = result * base / PRECISION;
        }
        base = base * base / PRECISION;
        exponent /= 2;
    }
    return result;
}

function calculateThreshold(uint256 requestedAmount) public view returns (uint256) {
    uint256 vaultBalance = requestToken.balanceOf(vault);
    uint256 totalSupply = token.totalSupply();
    uint256 stakedTokensTotal = _getTotalStaked(); // сумма всех активных stakes

    // Базовый threshold
    uint256 baseThreshold = totalSupply * minThresholdStake / PRECISION;

    if (requestedAmount == 0) return baseThreshold;

    // Динамическая составляющая
    uint256 rho = maxRatio * requestedAmount / vaultBalance;
    if (rho >= PRECISION) return type(uint256).max; // запрос превышает max ratio

    uint256 availableSupply = totalSupply - stakedTokensTotal;
    // threshold растёт квадратично при увеличении requestedAmount
    uint256 convictionFactor = totalSupply * totalSupply / (availableSupply * availableSupply + 1);

    return baseThreshold + convictionFactor * rho / PRECISION;
}

Precision и overflow

Ключевая уязвимость в conviction voting — overflow при вычислении convictionLast * alphaPow. Если conviction хранится в единицах токенов (18 decimals) и alpha тоже в high precision, промежуточные вычисления могут переполнить uint256.

Решение: нормализовать conviction в единицах без decimals, или использовать mulDiv из OpenZeppelin Math:

// Безопасное вычисление с защитой от overflow
proposal.convictionLast = Math.mulDiv(proposal.convictionLast, alphaPow, PRECISION)
    + Math.mulDiv(proposal.stakedTokens, PRECISION - alphaPow, PRECISION);

Math.mulDiv вычисляет (a * b) / c без промежуточного overflow, используя 512-bit arithmetic.

Параметры системы

Выбор параметров — не техническая, а экономическая задача:

Параметр Низкое значение Высокое значение Влияние
alpha 0.5-0.7 0.9-0.95 Скорость накопления; низкое = быстро, но легко манипулировать
maxRatio 5-10% 25-40% Максимальный размер одного grant
minThreshold 1-2% 5-10% Барьер для мелких proposals

Стандартные параметры Gardens: alpha = 0.9, maxRatio = 20%, minThreshold = 2%.

Alpha — самый чувствительный параметр. При alpha = 0.9 и периоде обновления 1 блок (12 сек) на Ethereum: полное накопление conviction до 95% максимума занимает ~28 дней. Это даёт времени достаточно для нарастания поддержки, но делает систему медленной. Для L2 с быстрыми блоками — нужно нормализовать alpha к реальному времени, а не блокам.

Delegation в conviction voting

Базовая реализация — пользователь голосует своим балансом. Делегирование добавляет сложность:

mapping(address => address) public delegate;
mapping(address => uint256) public delegatedAmount;

function delegateVotes(address to, uint256 amount) external {
    require(to != msg.sender, "Cannot self-delegate here"); // используй прямой stake
    token.transferFrom(msg.sender, address(this), amount);
    delegatedAmount[to] += amount;
    delegate[msg.sender] = to;
    // delegate теперь может использовать amount при stakeToProposal
}

Полноценное conviction voting с делегированием значительно сложнее: нужно отслеживать delegation chains, предотвращать circular delegation, корректно обновлять conviction при изменении delegation.

Для MVP: начать без delegation. Добавить в v2 при реальном запросе сообщества.

Отличие от стандартного Governor

Аспект Governor (snapshot) Conviction Voting
Голосование Фиксированный период Непрерывное
Принятие Большинство голосов Накопленный порог
Last-minute manipulation Возможна Затруднена (нужно время)
Voter apathy Критична (нет явки = нет decision) Менее критична
Voter apathy риск Низкий порог пропускает всё Proposals накапливают conviction
Параллельные proposals Каждый отдельно Конкурируют за stake

Conviction voting создаёт органическое приоритизирование: proposals, получившие наибольшую поддержку сообщества со временем, проходят первыми. Это особенно хорошо для grants DAO с постоянным потоком запросов.

Фронтенд: визуализация убеждённости

UX conviction voting — отдельная задача. Пользователь должен видеть:

  • График нарастания conviction для каждого proposal (линия, приближающаяся к threshold)
  • Текущее расстояние до threshold (в процентах и в единицах conviction)
  • Сколько дней нужно при текущем количестве votes
  • Свои stakes распределённые по proposals (сумма должна быть <= баланс)
// Вычисление conviction в фронтенде для real-time отображения
function computeConviction(
  lastConviction: bigint,
  lastBlock: number,
  currentBlock: number,
  stakedTokens: bigint,
  alpha: bigint,
  precision: bigint
): bigint {
  const blocksPassed = BigInt(currentBlock - lastBlock);
  const alphaPow = fastPow(alpha, blocksPassed, precision);

  return (lastConviction * alphaPow / precision)
    + (stakedTokens * (precision - alphaPow) / precision);
}

// Вычисление времени до прохождения при текущем stake
function timeToPass(
  currentConviction: bigint,
  threshold: bigint,
  stakedTokens: bigint,
  alpha: bigint,
  precision: bigint,
  blocksPerDay: number
): number {
  // conviction_max = stakedTokens * precision / (precision - alpha)
  const maxConviction = stakedTokens * precision / (precision - alpha);
  if (maxConviction < threshold) return Infinity; // никогда не достигнет

  // Решаем: threshold = maxConviction - (maxConviction - currentConviction) * alpha^t
  // t = log((maxConviction - threshold) / (maxConviction - currentConviction)) / log(alpha)
  const numerator = Number(maxConviction - threshold);
  const denominator = Number(maxConviction - currentConviction);
  const alphaNum = Number(alpha) / Number(precision);

  const blocks = Math.log(numerator / denominator) / Math.log(alphaNum);
  return blocks / blocksPerDay; // в днях
}

Recharts или D3.js для графика conviction curve — показываем текущую точку и прогнозируемую траекторию.

Процесс работы

Дизайн (1 неделя). Параметры alpha, maxRatio, minThreshold — под конкретную DAO и размер treasury. Интеграция с существующим токеном или новый токен. Vault архитектура.

Разработка контрактов (3-4 недели). ConvictionVoting + Vault + параметры governance (изменяемые через timelock) + comprehensive тесты включая precision edge cases.

Симуляция параметров (3-5 дней). Python/TypeScript симуляция: как быстро проходят proposals при разных параметрах, насколько эффективна защита от whale manipulation.

Аудит (2 недели). Особое внимание: overflow в conviction вычислениях, корректность threshold формулы, reentrancy в executeProposal.

Frontend (2-3 недели). Dashboard proposals, stake management, conviction visualization, мобильная адаптация.

Полный цикл: 2,5-3 месяца. Стоимость — после уточнения параметров и scope.