Розробка системи reputation-weighted голосування
Token-weighted voting має фундаментальний недолік: покупку голосовою силою за гроші. Багатий учасник не зобов'язаний розбиратися в предметі — він просто перекриває всіх. Reputation-weighted voting вирішує цю проблему інакше: вага голосу визначається історією участі, якістю минулих рішень та внеском у протокол, не балансом кошелька.
Це значно складніше в реалізації. Потрібно вирішити три нетривіальні задачі: як вимірити репутацію без маніпуляцій, як зберігати та оновлювати її on-chain ефективно та як запобігти накопленню репутації через sybil атаки.
Моделі репутації: звідки береться вага
Репутація може будуватися з кількох джерел одночасно.
On-chain активність: частота та якість голосувань, своєчасність участі, слідування рішенням (голосував з більшістю vs contrarian). Останнє спірно — іноді праві саме несогласні.
Contribution-based: merged PR в протокол, написані propozals, організовані community calls, написані документи. Введення contribution складніше автоматизувати, потребує суб'єктивної оцінки.
Peer review: інші учасники оцінюють вклад, репутація будується соціально. Це модель SourceCred — граф розподілу репутації через взаємні оцінки.
Outcome-based: ретроактивна оцінка рішень. Якщо пропозал, за який голосував учасник, принес вимірюване користі протоколу — його репутаційна вага зростає. Потребує oracle метрик.
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; // створені propozals
uint256 proposalsPassed; // прийняті propozals
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 голосувань поспіль). Знижує тривогу для випадкових пропусків.
# Моделювання 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, але різні права (створювати propozals, накладати veto та т.д.).
| Репутація | Тир | Права |
|---|---|---|
| 0–99 | Observer | Тільки голосування |
| 100–499 | Participant | Голос + коментарі в propozals |
| 500–1999 | Member | Створення propozals |
| 2000–9999 | Core | Veto право, зміни параметрів |
| 10000+ | Maintainer | Екстренні дії |
Делегування репутації
Репутацію не можна продати (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];
}
}
Делегування критично для протоколів з технічними рішеннями: багато токен-холдерів не розбиратимуться в деталях конкретного пропозалу, вони делегують довіреним експертам.
Інтеграція з 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 механізм — обов'язковий елемент. Governor перевіряє voting power на блоці створення пропозалу, не на поточному блоці. Без чекпоінтів система не працює коректно.
Anti-sybil захист
Репутаційна система особливо уразлива до sybil атак: створити 100 адрес з мінімальною репутацією простіше, ніж набрати велику репутацію на одному адресі.
Proof of Humanity / Worldcoin: верифікація унікальності через biometrics. Кожна верифікована людина отримує один репутаційний аккаунт. Жорсткий, але ефективний.
Gitcoin Passport: м'який підхід. Агрегація identity proof з багатьох джерел (ENS, GitHub, Twitter, Lens та ін.). Чим більш джерел — тим вищий «humanness score». Нижній поріг для отримання репутації.
Social graph analysis: учасники з однаковим паттерном голосування можуть бути однією людиною. Статистичний аналіз on-chain поведінки.
Stake-based admission: для отримання початкової репутації потребує застейкати невелику суму. Створює фінансовий бар'єр для sybil при low cost на real accounts.
Governance параметри та їх настройка
Reputation-weighted governance вимагає ретельної настройки числових параметрів. Неправильні значення призводять або до централізації (кілька учасників з величезною репутацією контролюють все), або до паралічу (репутація розподілена так рівномірно, що кворум недосяжний).
Quorum: мінімальний % від total voting power для валідності голосування. Для reputation-weighted систем зазвичай нижче (5–10%), ніж для token-weighted (вимагаючих 4–20% circulating supply).
Proposal threshold: мінімальний voting power для створення пропозалу. Повинен бути достатнім, щоб відсіяти спам, але не настільки високим, щоб виключити нових активних учасників.
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 для створення пропозалу
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, що беруть участь у середньому пропозалу. Цільовий показник — 20–40%.
New entrant mobility: як швидко новий активний учасник набирає значиму голосову силу. Якщо це займає >12 місяців — система закрита для newcomers.
Proposal success rate: % propozals, що досягли кворума. Занадто низький (< 50%) — кворум завищений. Занадто високий (> 95%) — кворум занижений або сообщество занадто однорідне.
Стек і сроки розробки
Смарт-контракти: Solidity + OpenZeppelin Governor + кастомний IVotes. Hardhat/Foundry для тестування. 10–14 тижнів включаючи checkpoint mechanism та тести.
Off-chain інфраструктура: індексер (The Graph subgraph) для історичних даних репутації, API для frontend, сервіс нарахування репутації за off-chain вклади.
Frontend: DAO dashboard з репутаційними профілями, lifecycle propozals, аналітикою. React + wagmi.
Аудит: обов'язковий. Логіка нарахування та decay репутації — потенціальна точка атаки.
Повний цикл розробки: 5–8 місяців для production-ready системи з аудитом.







