Разработка системы reputation-weighted голосования
Token-weighted voting имеет фундаментальный изъян: покупка голосовой силы за деньги. Богатый участник не обязан разбираться в предмете — он просто перекрывает всех. Reputation-weighted voting решает эту проблему иначе: вес голоса определяется историей участия, качеством прошлых решений и вкладом в протокол, а не балансом кошелька.
Это значительно сложнее в реализации. Нужно решить три нетривиальные задачи: как измерить репутацию без манипуляций, как хранить и обновлять её on-chain эффективно, и как предотвратить накопление репутации через sybil-атаки.
Модели репутации: откуда берётся вес
Репутация может строиться из нескольких источников одновременно.
On-chain активность: частота и качество голосований, своевременность participation, следование решениям (voted with majority vs contrarian). Последнее спорно — иногда правы именно несогласные.
Contribution-based: смерженные PR в протокол, написанные proposals, организованные community calls, написанные документы. Ввод contribution сложнее автоматизировать, требует субъективной оценки.
Peer review: другие участники оценивают вклад, репутация строится социально. Это модель SourceCred — grafo распределения репутации через взаимные оценки.
Outcome-based: ретроактивная оценка решений. Если предложение, за которое голосовал участник, принесло измеримую пользу протоколу — его репутационный вес растёт. Требует оракула метрик.
Soulbound tokens как носитель репутации
EIP-5114 (Soulbound tokens) — нетрансферабельные NFT, привязанные к адресу. Идеальный носитель для репутации: нельзя купить и продать, нельзя делегировать чужую репутацию.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ReputationToken {
struct ReputationData {
uint256 baseScore; // накопленная репутация
uint256 participationCount; // число голосований
uint256 proposalsCreated; // созданные предложения
uint256 proposalsPassed; // принятые предложения
uint256 lastActivityBlock;
uint256 decayFactor; // множитель decay (1000 = 1.0)
bool exists;
}
mapping(address => ReputationData) public reputation;
mapping(address => bool) public trustedIssuers; // кто может выдавать репутацию
uint256 public constant DECAY_PERIOD = 180 days;
uint256 public constant DECAY_RATE = 50; // 5% в период
uint256 public constant BASE_VOTE_SCORE = 10;
uint256 public constant PROPOSAL_BONUS = 100;
uint256 public constant PASSED_PROPOSAL_BONUS = 500;
event ReputationEarned(address indexed participant, uint256 amount, string reason);
event ReputationDecayed(address indexed participant, uint256 newScore);
modifier onlyIssuer() {
require(trustedIssuers[msg.sender], "Not a trusted issuer");
_;
}
// Soulbound: transfer заблокирован на уровне контракта
// Реализуем через ERC-721 без функции transfer
function awardParticipation(address participant, uint256 pollId) external onlyIssuer {
_ensureExists(participant);
_applyDecay(participant);
ReputationData storage rep = reputation[participant];
rep.baseScore += BASE_VOTE_SCORE;
rep.participationCount++;
rep.lastActivityBlock = block.number;
emit ReputationEarned(participant, BASE_VOTE_SCORE, "participation");
}
function awardProposalCreation(address participant, bool passed) external onlyIssuer {
_ensureExists(participant);
_applyDecay(participant);
ReputationData storage rep = reputation[participant];
uint256 bonus = passed ? PASSED_PROPOSAL_BONUS : PROPOSAL_BONUS;
rep.baseScore += bonus;
rep.proposalsCreated++;
if (passed) rep.proposalsPassed++;
rep.lastActivityBlock = block.number;
emit ReputationEarned(participant, bonus, passed ? "passed_proposal" : "created_proposal");
}
function awardContribution(
address participant,
uint256 amount,
string calldata reason
) external onlyIssuer {
_ensureExists(participant);
_applyDecay(participant);
reputation[participant].baseScore += amount;
emit ReputationEarned(participant, amount, reason);
}
function getVotingPower(address participant) external view returns (uint256) {
if (!reputation[participant].exists) return 0;
ReputationData memory rep = reputation[participant];
uint256 currentScore = _calculateCurrentScore(participant);
// Нелинейное масштабирование: предотвращает монополизацию
// power = sqrt(score) * 100, нормализуем к базе 1000
return _sqrt(currentScore) * 100;
}
function _applyDecay(address participant) internal {
ReputationData storage rep = reputation[participant];
if (!rep.exists) return;
uint256 inactiveTime = block.timestamp -
(rep.lastActivityBlock * 12); // ~12 sec per block
if (inactiveTime > DECAY_PERIOD) {
uint256 periods = inactiveTime / DECAY_PERIOD;
uint256 decay = (1000 - DECAY_RATE) ** periods / (1000 ** (periods - 1));
rep.baseScore = rep.baseScore * decay / 1000;
emit ReputationDecayed(participant, rep.baseScore);
}
}
function _calculateCurrentScore(address participant) internal view returns (uint256) {
ReputationData memory rep = reputation[participant];
uint256 inactiveTime = block.timestamp - (rep.lastActivityBlock * 12);
if (inactiveTime <= DECAY_PERIOD) return rep.baseScore;
uint256 periods = inactiveTime / DECAY_PERIOD;
uint256 score = rep.baseScore;
for (uint256 i = 0; i < periods && score > 0; i++) {
score = score * (1000 - DECAY_RATE) / 1000;
}
return score;
}
function _sqrt(uint256 x) internal pure returns (uint256) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2;
uint256 y = x;
while (z < y) {
y = z;
z = (x / z + z) / 2;
}
return y;
}
function _ensureExists(address participant) internal {
if (!reputation[participant].exists) {
reputation[participant].exists = true;
reputation[participant].decayFactor = 1000;
reputation[participant].lastActivityBlock = block.number;
}
}
}
Decay механизм: борьба с накопленной властью
Без decay репутация накапливается и не исчезает. Участник, активный три года назад, сохраняет доминирующий вес вечно. Это нарушает инклюзивность: новые участники никогда не смогут войти в круг принятия решений.
Decay — ключевой элемент репутационной системы. Репутация уменьшается при неактивности. Смысл: вы должны продолжать участвовать, чтобы сохранять влияние.
Линейный decay: простейший вариант. -X% в месяц при отсутствии активности. Предсказуем, но грубоват.
Экспоненциальный decay: более естественная модель. Репутация уменьшается быстрее при длительной неактивности. Параметры подбираются под сообщество.
Activity-gated decay: decay начинается только после порога неактивности (например, пропуск более 3 голосований подряд). Снижает anxiety для случайных пропусков.
# Моделирование decay для подбора параметров
import numpy as np
import matplotlib.pyplot as plt
def simulate_reputation(
initial_score: float,
monthly_activity_score: float, # сколько репутации зарабатывает активный участник
decay_rate: float, # коэффициент decay за период неактивности
decay_period_months: int,
simulation_months: int = 60,
active_months: list = None # месяцы с активностью
) -> list:
"""
Симуляция репутации участника
active_months: список месяцев, когда участник был активен
"""
if active_months is None:
active_months = list(range(simulation_months))
score = initial_score
history = [score]
inactive_periods = 0
for month in range(1, simulation_months):
if month in active_months:
score += monthly_activity_score
inactive_periods = 0
else:
inactive_periods += 1
if inactive_periods >= decay_period_months:
score *= (1 - decay_rate)
history.append(max(0, score))
return history
# Пример: участник активен первые 12 месяцев, потом пропадает на год, возвращается
active = list(range(12)) + list(range(24, 36))
scores = simulate_reputation(
initial_score=0,
monthly_activity_score=50,
decay_rate=0.08, # 8% в период неактивности
decay_period_months=3,
simulation_months=48,
active_months=active
)
Подбирать параметры decay стоит так, чтобы: активный участник поддерживал stable score, три месяца неактивности снижали score на ~20–30%, возврат к участию давал возможность восстановить позицию за разумное время.
Voting Power Computation: нелинейное масштабирование
Линейное соответствие репутации голосовой силе воспроизводит проблему токен-weighted voting: тот, у кого наибольшая репутация, полностью доминирует.
Квадратный корень: voting_power = √(reputation_score). Классика quadratic voting. Человек с репутацией 10,000 имеет силу 100, человек с репутацией 100 — силу 10. Разрыв сохраняется, но пропорционально меньше.
Логарифмический масштаб: voting_power = log2(reputation_score + 1) * scale. Ещё более сглаженный. При очень большом spread репутации — оптимальный выбор.
Threshold тиры: репутация делит участников на категории (Junior, Member, Senior, Core). Внутри категории — equal voting power, но разные права (создавать proposals, накладывать veto и т.д.).
| Репутация | Тир | Права |
|---|---|---|
| 0–99 | Наблюдатель | Только голосование |
| 100–499 | Участник | Голос + комментарии в proposals |
| 500–1999 | Член | Создание proposals |
| 2000–9999 | Ядро | Veto право, временные параметры |
| 10000+ | Мейнтейнер | Экстренные действия |
Делегирование репутации
Репутацию нельзя продать (soulbound), но можно делегировать голосовую силу — аналогично delegated voting в Governor Bravo. Делегат голосует от вашего имени, но репутация остаётся у вас.
contract ReputationDelegation {
mapping(address => address) public delegates;
mapping(address => uint256) public delegatedPower;
function delegate(address delegatee) external {
address previousDelegate = delegates[msg.sender];
uint256 myPower = reputationToken.getVotingPower(msg.sender);
// Убираем делегирование у предыдущего делегата
if (previousDelegate != address(0)) {
delegatedPower[previousDelegate] -= myPower;
}
delegates[msg.sender] = delegatee;
if (delegatee != address(0)) {
delegatedPower[delegatee] += myPower;
}
emit DelegateChanged(msg.sender, previousDelegate, delegatee);
}
function getEffectivePower(address account) public view returns (uint256) {
return reputationToken.getVotingPower(account) + delegatedPower[account];
}
}
Делегирование критично для протоколов с техническими решениями: многие токен-держатели не разбираются в деталях конкретного proposal, они делегируют доверенным экспертам.
Интеграция с Governor
OpenZeppelin Governor поддерживает кастомный расчёт voting power через интерфейс IVotes. Репутационный контракт должен его реализовывать:
contract ReputationVotes is IVotes, ReputationToken {
function getVotes(address account) external view override returns (uint256) {
return getEffectivePower(account);
}
function getPastVotes(address account, uint256 blockNumber)
external view override returns (uint256)
{
// Для точности нужен checkpoint mechanism
return _getPastVotingPower(account, blockNumber);
}
function getPastTotalSupply(uint256 blockNumber)
external view override returns (uint256)
{
return _getPastTotalPower(blockNumber);
}
// Checkpoint: снапшоты voting power для каждого блока
// Используем паттерн ERC20Votes из OpenZeppelin
}
Checkpoint mechanism — обязательный элемент. Governor проверяет voting power на блоке создания proposal, не на текущем блоке. Без чекпоинтов система не работает корректно.
Anti-sybil защита
Репутационная система особенно уязвима к sybil-атакам: создать 100 адресов с минимальной репутацией проще, чем набрать большую репутацию на одном.
Proof of Humanity / Worldcoin: верификация уникальности через biometrics. Каждый verified человек получает один репутационный аккаунт. Жёсткий, но эффективный.
Gitcoin Passport: мягкий подход. Агрегация identity proof из множества источников (ENS, GitHub, Twitter, Lens и др.). Чем больше источников — тем выше «humanness score». Нижний порог для получения репутации.
Social graph analysis: участники с одинаковым паттерном голосования могут быть одним человеком. Статистический анализ on-chain поведения.
Stake-based admission: для получения начальной репутации требуется застейкать небольшую сумму. Создаёт финансовый барьер для sybil при low cost on real accounts.
Governance параметры и их настройка
Reputation-weighted governance требует тщательной настройки числовых параметров. Неправильные значения приводят либо к централизации (несколько участников с огромной репутацией контролируют всё), либо к параличу (репутация распределена так равномерно, что кворум недостижим).
Quorum: минимальный % от total voting power для валидности голосования. Для reputation-weighted систем обычно ниже (5–10%), чем для token-weighted (требующих 4–20% circulating supply).
Proposal threshold: минимальный voting power для создания proposal. Должен быть достаточен, чтобы отсеять спам, но не настолько высок, чтобы исключить новых активных участников.
Timelock: обязателен. Даже при децентрализованном governance критически важно иметь время на реакцию сообщества перед исполнением.
// Пример конфигурации для среднего DAO
const governanceConfig = {
// Репутационные параметры
baseVoteScore: 10, // очков за каждый голос
proposalCreationBonus: 100,
passedProposalBonus: 500,
contributionMultiplier: 1.5, // x для верифицированных вкладов
// Decay
decayPeriodDays: 90, // 3 месяца неактивности = начало decay
decayRatePerPeriod: 0.07, // 7% за каждый период неактивности
// Voting
quorumPercent: 8, // 8% от total voting power
proposalThreshold: 200, // минимальный score для создания proposal
votingPeriodDays: 7,
timelockDays: 2,
// Anti-sybil
minPassportScore: 15, // Gitcoin Passport minimum score
minStakeAmount: '100', // 100 токенов для admission
};
Мониторинг и аналитика
Reputation-weighted система требует постоянного мониторинга здоровья governance:
Концентрация: индекс Джини voting power. Если Gini > 0.7 — система де-факто централизована.
Participation rate: % от eligible voters, участвующих в среднем proposal. Целевой показатель — 20–40%.
New entrant mobility: как быстро новый активный участник набирает значимую голосовую силу. Если это занимает >12 месяцев — система закрыта для newcomers.
Proposal success rate: % proposals, достигших кворума. Слишком низкий (< 50%) — кворум завышен. Слишком высокий (> 95%) — кворум занижен или сообщество слишком однородно.
Стек и сроки разработки
Смарт-контракты: Solidity + OpenZeppelin Governor + кастомный IVotes. Hardhat/Foundry для тестирования. 10–14 недель включая checkpoint mechanism и тесты.
Off-chain инфраструктура: индексер (The Graph subgraph) для исторических данных репутации, API для frontend, сервис начисления репутации за off-chain вклады.
Frontend: DAO dashboard с репутационными профилями, proposal lifecycle, аналитикой. React + wagmi.
Аудит: обязателен. Логика начисления и decay репутации — потенциальная точка атаки.
Полный цикл разработки: 5–8 месяцев для production-ready системы с аудитом.







