Developing DAO delegate system
Delegation solves on-chain governance's voter apathy problem. Most DAOs see real turnout of 5–15% circulating supply. Reasons: technical barriers (keep tokens unlocked + pay gas), information load (can't analyze every proposal), lack of motivation for small holders.
Delegation system lets token holders delegate voting power to specialized delegates — active community members who regularly vote and publicly justify positions. This is closer to representative democracy than direct voting.
Basic mechanism: ERC-20Votes delegation
Standard ERC-20Votes (OpenZeppelin) provides built-in delegation via checkpoint system.
// ERC-20Votes adds:
// - delegate(address delegatee) — delegate voting power
// - delegates(address account) → address — current delegate
// - getVotes(address account) → uint256 — current voting power
// - getPastVotes(address account, uint256 blockNumber) → uint256
// - getPastTotalSupply(uint256 blockNumber) → uint256
// Example: holder delegates all voting power
token.delegate(delegateAddress);
// Self-delegation (needed to get own voting power)
token.delegate(msg.sender);
Critical moment: after buying tokens, user has zero voting power until calling delegate. This unintuitive behavior must be explicitly shown in UI. Many large Uniswap holders don't participate in governance simply because they never delegated.
Checkpoint mechanism: delegate records voting power snapshot at call time. Governor uses getPastVotes(voter, proposalSnapshot) — voting power at proposal creation block. Buying tokens after snapshot doesn't give power in current vote.
Partial delegation
Basic ERC-20Votes supports delegating only wholly to one address. For advanced systems you need partial: 40% to delegate A, 30% to delegate B, 30% keep self.
ERC-20VotesPartialDelegation (EIP-5805 extension)
struct Delegation {
address delegatee;
uint96 numerator; // fraction in numerator/denominator
}
// User delegates fractionally
function delegateMulti(Delegation[] calldata delegations) external {
uint256 totalNumerator;
for (uint i = 0; i < delegations.length; i++) {
totalNumerator += delegations[i].numerator;
}
require(totalNumerator <= DENOMINATOR, "Exceeds 100%");
_updateDelegations(msg.sender, delegations);
}
This significantly complicates checkpoint logic: each transfer must recalculate contributions to each delegate. Gas costs grow with delegate count.
Ready implementations: Compound Governor Bravo supports basic delegation; for fractional — look at OpenZeppelin GovernorCountingFractional + custom token.
Delegate profiles and reputation
Technical delegation mechanism is one thing. Infrastructure for deciding whom to delegate to — another.
On-chain delegate registry
contract DelegateRegistry {
struct DelegateProfile {
string name;
string ipfsMetadata; // CIDv1 hash of IPFS doc with full profile
string[] interests; // tags: ["defi", "security", "tokenomics"]
uint256 createdAt;
}
mapping(address => DelegateProfile) public profiles;
event ProfileUpdated(address indexed delegate, string ipfsMetadata);
function setProfile(
string calldata name,
string calldata ipfsMetadata,
string[] calldata interests
) external {
profiles[msg.sender] = DelegateProfile({
name: name,
ipfsMetadata: ipfsMetadata,
interests: interests,
createdAt: block.timestamp
});
emit ProfileUpdated(msg.sender, ipfsMetadata);
}
}
IPFS metadata contains: platform statement (position on key questions), voting history with rationales, contact info. Storing full profile off-chain (IPFS) reduces gas, on-chain only hash for verification.
Delegate activity statistics
Real delegate value is track record. Subgraph should index:
- Participation rate: % of proposals voted on during delegation period
- Alignment: how often delegate position matches final result (not best metric, but informative)
- Rationale rate: % of votes with published reasoning
// The Graph subgraph mapping
export function handleVoteCast(event: VoteCast): void {
let delegate = Delegate.load(event.params.voter.toHex());
if (!delegate) {
delegate = new Delegate(event.params.voter.toHex());
delegate.totalVotes = BigInt.zero();
delegate.participationRate = BigDecimal.zero();
}
delegate.totalVotes = delegate.totalVotes.plus(BigInt.fromI32(1));
delegate.save();
let vote = new Vote(event.transaction.hash.toHex() + '-' + event.logIndex.toString());
vote.delegate = delegate.id;
vote.proposalId = event.params.proposalId;
vote.support = event.params.support;
vote.weight = event.params.weight;
vote.reason = event.params.reason;
vote.save();
}
Gasless delegation via EIP-712
Delegation requires on-chain transaction. At $2–5 gas cost this is barrier for small holders. Solution — delegateBySig: holder signs delegation off-chain, relayer sends transaction and pays gas.
// Built into ERC-20Votes (OpenZeppelin)
function delegateBySig(
address delegatee,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(block.timestamp <= expiry, "Expired");
address signer = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(
DELEGATION_TYPEHASH,
delegatee,
nonce,
expiry
))),
v, r, s
);
require(nonce == _useNonce(signer), "Invalid nonce");
_delegate(signer, delegatee);
}
Frontend flow: user signs EIP-712 message (MetaMask shows readable text "Delegate [address] until [date]"), signature sent to backend, backend calls delegateBySig and pays gas.
Batch gasless delegation for onboarding
When onboarding new user can request delegate-signature for multiple tokens:
const delegations = await Promise.all([
signDelegation(tokenA, delegatee, signer),
signDelegation(tokenB, delegatee, signer),
]);
// Single relayer submit
await Promise.all(delegations.map(d => relayer.submit(d)));
Revoking delegation and re-delegation
Delegation is not permanent. delegate(address(0)) or delegate(msg.sender) revokes delegation. Changing delegate: delegate(newDelegatee) automatically transfers all voting power.
Effective time: change effective next block. If proposal already created and snapshot fixed — change doesn't affect current vote.
UI recommendations: show user current delegate, their participation rate last 30 days, deadline of nearest active votes. This reduces churn of delegators disappointed by inactive delegate.
Incentive mechanics for delegates
Pure altruistic model works only for large holders. For systemic work you need incentives.
Delegate rewards. Protocol pays delegates for active participation. Calculation: participation rate × delegated voting power × reward rate. Distributed from treasury through governance-approved contract.
Streaming payments (Sablier/SuperFluid). Delegate gets constant stream while participation rate above threshold. Below threshold — stream auto-pauses via keeper.
// Keeper checks participation rate
function checkAndUpdateDelegateStream(address delegate) external {
uint256 participationRate = stats.getParticipationRate(delegate, 30 days);
if (participationRate < MIN_PARTICIPATION_RATE) {
sablier.cancel(delegateStreams[delegate]);
emit DelegateStreamPaused(delegate, participationRate);
}
}
Stack and timeline
Contracts: ERC-20Votes extension + DelegateRegistry + reward distributor. Solidity + Foundry + OpenZeppelin 5.x. Indexing: The Graph subgraph for delegate stats. Frontend: React + wagmi + react-query.
| Component | Timeline |
|---|---|
| ERC-20Votes + gasless delegation | 1 week |
| DelegateRegistry on-chain | 3–5 days |
| The Graph subgraph for stats | 1 week |
| Frontend: delegate search, delegation UI | 2 weeks |
| Reward mechanics | 1–2 weeks |
MVP (basic delegation + gasless + delegate profiles UI): 4–5 weeks. Full system with reward streaming and analytics: 2 months.







