Developing reputation-weighted voting system
Token-weighted voting has a fundamental flaw: buying voting power with money. A wealthy participant doesn't need to understand the subject — they just override everyone. Reputation-weighted voting solves this differently: voting weight is determined by history of participation, quality of past decisions, and protocol contribution, not wallet balance.
This is significantly more complex to implement. You need to solve three non-trivial problems: how to measure reputation without manipulation, how to store and update it on-chain efficiently, and how to prevent reputation accumulation through sybil attacks.
Reputation models: where weight comes from
Reputation can be built from multiple sources simultaneously.
On-chain activity: frequency and quality of votes, timeliness of participation, following decisions (voted with majority vs contrarian). The last is debatable — sometimes the dissenters are right.
Contribution-based: merged PRs into protocol, written proposals, organized community calls, written documentation. Entering contributions is harder to automate, requires subjective evaluation.
Peer review: other participants rate contributions, reputation builds socially. This is the SourceCred model — graph distribution of reputation through mutual ratings.
Outcome-based: retroactive evaluation of decisions. If a proposal the participant voted for brought measurable benefit to the protocol — their reputation weight grows. Requires a metrics oracle.
Soulbound tokens as reputation carrier
EIP-5114 (Soulbound tokens) — non-transferable NFTs tied to an address. Ideal reputation carrier: can't be bought and sold, can't delegate someone else's reputation.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ReputationToken {
struct ReputationData {
uint256 baseScore; // accumulated reputation
uint256 participationCount; // number of votes
uint256 proposalsCreated; // created proposals
uint256 proposalsPassed; // passed proposals
uint256 lastActivityBlock;
uint256 decayFactor; // decay multiplier (1000 = 1.0)
bool exists;
}
mapping(address => ReputationData) public reputation;
mapping(address => bool) public trustedIssuers; // who can issue reputation
uint256 public constant DECAY_PERIOD = 180 days;
uint256 public constant DECAY_RATE = 50; // 5% per period
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 is blocked at contract level
// Implement through ERC-721 without transfer function
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);
// Nonlinear scaling: prevents monopolization
// power = sqrt(score) * 100, normalized to base 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 mechanism: fighting accumulated power
Without decay, reputation accumulates and never disappears. A participant active three years ago retains dominant weight forever. This violates inclusiveness: new participants can never enter the decision-making circle.
Decay is a key element of reputation systems. Reputation decreases with inactivity. The idea: you must continue participating to maintain influence.
Linear decay: simplest variant. -X% per month without activity. Predictable, but crude.
Exponential decay: more natural model. Reputation decreases faster with prolonged inactivity. Parameters are tuned to the community.
Activity-gated decay: decay starts only after inactivity threshold (e.g., missing more than 3 votes in a row). Reduces anxiety for occasional misses.
# Decay modeling for parameter tuning
import numpy as np
import matplotlib.pyplot as plt
def simulate_reputation(
initial_score: float,
monthly_activity_score: float, # reputation earned by active participant
decay_rate: float, # decay coefficient per inactivity period
decay_period_months: int,
simulation_months: int = 60,
active_months: list = None # months with activity
) -> list:
"""
Simulate participant reputation
active_months: list of months when participant was active
"""
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
# Example: participant active first 12 months, disappears for a year, returns
active = list(range(12)) + list(range(24, 36))
scores = simulate_reputation(
initial_score=0,
monthly_activity_score=50,
decay_rate=0.08, # 8% per inactivity period
decay_period_months=3,
simulation_months=48,
active_months=active
)
Good parameter tuning ensures: active participant maintains stable score, three months inactivity reduces score by ~20–30%, return to participation allows quick position recovery.
Voting Power Computation: nonlinear scaling
Linear reputation-to-voting-power mapping reproduces token-weighted voting problem: whoever has most reputation dominates completely.
Square root: voting_power = √(reputation_score). Classic quadratic voting. Person with 10,000 reputation has power 100, person with 100 reputation has power 10. Gap persists, but proportionally smaller.
Logarithmic scale: voting_power = log2(reputation_score + 1) * scale. Even more smoothed. For very large reputation spread — optimal choice.
Threshold tiers: reputation divides participants into categories (Junior, Member, Senior, Core). Inside category — equal voting power, but different rights (create proposals, veto, etc).
| Reputation | Tier | Rights |
|---|---|---|
| 0–99 | Observer | Voting only |
| 100–499 | Participant | Vote + proposal comments |
| 500–1999 | Member | Create proposals |
| 2000–9999 | Core | Veto right, parameter changes |
| 10000+ | Maintainer | Emergency actions |
Reputation Delegation
Reputation can't be sold (soulbound), but voting power can be delegated — similar to delegated voting in Governor Bravo. Delegate votes on your behalf, but reputation stays with you.
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);
// Remove delegation from previous delegate
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];
}
}
Delegation is critical for protocols with technical decisions: many token holders can't dive into proposal details, they delegate to trusted experts.
Integration with Governor
OpenZeppelin Governor supports custom voting power calculation through IVotes interface. Reputation contract must implement it:
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)
{
// For accuracy need checkpoint mechanism
return _getPastVotingPower(account, blockNumber);
}
function getPastTotalSupply(uint256 blockNumber)
external view override returns (uint256)
{
return _getPastTotalPower(blockNumber);
}
// Checkpoint: snapshots of voting power for each block
// Use ERC20Votes pattern from OpenZeppelin
}
Checkpoint mechanism is mandatory. Governor checks voting power at proposal snapshot block, not current block. Without checkpoints the system doesn't work correctly.
Anti-sybil protection
Reputation systems are especially vulnerable to sybil attacks: creating 100 addresses with minimal reputation is easier than building high reputation on one account.
Proof of Humanity / Worldcoin: uniqueness verification via biometrics. Each verified person gets one reputation account. Harsh, but effective.
Gitcoin Passport: soft approach. Aggregates identity proofs from many sources (ENS, GitHub, Twitter, Lens, etc). More sources = higher "humanness score". Minimum threshold for reputation eligibility.
Social graph analysis: participants with identical voting patterns may be one person. Statistical on-chain behavior analysis.
Stake-based admission: to earn initial reputation requires staking a small amount. Creates financial barrier for sybil while not expensive for real accounts.
Governance parameters and tuning
Reputation-weighted governance requires careful numerical parameter tuning. Wrong values lead to either centralization (few high-reputation participants control everything) or paralysis (reputation too evenly distributed, quorum unreachable).
Quorum: minimum % of total voting power for validity. For reputation-weighted systems typically lower (5–10%) than token-weighted (needing 4–20% circulating supply).
Proposal threshold: minimum voting power to create proposal. Should be enough to filter spam but not exclude new active participants.
Timelock: mandatory. Even with decentralized governance, community reaction time before execution is critical.
// Example configuration for medium-sized DAO
const governanceConfig = {
// Reputation parameters
baseVoteScore: 10, // points per vote
proposalCreationBonus: 100,
passedProposalBonus: 500,
contributionMultiplier: 1.5, // x for verified contributions
// Decay
decayPeriodDays: 90, // 3 months inactivity = decay starts
decayRatePerPeriod: 0.07, // 7% per inactivity period
// Voting
quorumPercent: 8, // 8% of total voting power
proposalThreshold: 200, // min score to create proposal
votingPeriodDays: 7,
timelockDays: 2,
// Anti-sybil
minPassportScore: 15, // Gitcoin Passport minimum score
minStakeAmount: '100', // 100 tokens for admission
};
Monitoring and analytics
Reputation-weighted system requires constant governance health monitoring:
Concentration: Gini index of voting power. If Gini > 0.7 — system is de-facto centralized.
Participation rate: % of eligible voters participating in average proposal. Target — 20–40%.
New entrant mobility: how fast active newcomer accumulates meaningful voting power. If it takes >12 months — system is closed to newcomers.
Proposal success rate: % of proposals reaching quorum. Too low (< 50%) — quorum too high. Too high (> 95%) — quorum too low or community too uniform.
Stack and development timeline
Smart contracts: Solidity + OpenZeppelin Governor + custom IVotes. Hardhat/Foundry for testing. 10–14 weeks including checkpoint mechanism and tests.
Off-chain infrastructure: indexer (The Graph subgraph) for reputation history, API for frontend, service for off-chain contribution reputation.
Frontend: DAO dashboard with reputation profiles, proposal lifecycle, analytics. React + wagmi.
Audit: mandatory. Reputation earning and decay logic — potential attack point.
Full development cycle: 5–8 months for production-ready system with audit.







