Documentation Index
Fetch the complete documentation index at: https://mintlify.com/voteagora/agora-next/llms.txt
Use this file to discover all available pages before exploring further.
Governance tokens are the foundation of DAO voting in Agora. This guide covers delegation mechanics, voting power calculations, and multi-chain token aggregation.
Token Standards
Agora supports ERC-20 tokens with votes extension (ERC-20Votes) for delegation and voting power tracking:
interface ITokenContract {
function balanceOf(address account) external view returns (uint256);
function getVotes(address account) external view returns (uint256);
function delegate(address delegatee) external;
function delegateBySig(
address delegatee,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external;
}
Voting Power Calculation
Voting power is determined by the maximum of delegated votes or token balance:
// src/lib/votingPowerUtils.ts:32
export async function fetchVotingPowerFromContract(
client: PublicClient,
address: string,
config: VotingPowerConfig
): Promise<bigint> {
// Get delegated voting power
const votes = await client.readContract({
abi: config.contracts.token.abi,
address: config.contracts.token.address,
functionName: "getVotes",
args: [address],
});
// Get token balance
const balance = await client.readContract({
abi: config.contracts.token.abi,
address: config.contracts.token.address,
functionName: "balanceOf",
args: [address],
});
// Return maximum of votes and balance
return votes > balance ? votes : balance;
}
Voting power uses the maximum of getVotes() and balanceOf() to handle both delegated and self-delegated scenarios.
Delegation Models
Agora supports three delegation models, configured per tenant:
Full Delegation
Standard model where all voting power goes to a single delegate:
// src/lib/tenant/configs/contracts/ens.ts:76
delegationModel: DELEGATION_MODEL.FULL
// Delegate all tokens to one address
await tokenContract.delegate(delegateeAddress);
Partial Delegation
Split voting power across multiple delegates:
// src/lib/tenant/configs/contracts/scroll.ts:110
delegationModel: DELEGATION_MODEL.PARTIAL
With partial delegation, users can allocate specific amounts to different delegates. Implementation varies by DAO but typically uses a delegation registry contract.
Advanced Delegation (Alligator)
Supports subdelegation chains with custom rules:
// src/lib/tenant/configs/contracts/optimism.ts:119
delegationModel: DELEGATION_MODEL.ADVANCED,
governorType: GOVERNOR_TYPE.ALLIGATOR
Advanced delegation allows:
- Subdelegation chains - Delegates can redelegate to others
- Custom rules - Max redelegations, allowlists, blocklists
- Partial subdelegation - Split delegated power further
// Example subdelegation tracking
// src/lib/alligatorUtils.ts:41
interface SubdelegationData {
subdelegated_share: number; // Relative subdelegations
subdelegated_amount: number; // Absolute subdelegations
}
Token Configuration
Define token details in your tenant config:
// Token factory pattern
import TenantTokenFactory from "@/lib/tenant/tenantTokenFactory";
const token: TenantToken = {
name: "Optimism",
symbol: "OP",
decimals: 18,
address: "0x4200000000000000000000000000000000000042",
chainId: 10,
};
Multi-Chain Token Support
Aggregate voting power across multiple chains for the same governance token:
// src/lib/votingPowerUtils.ts:68
if (multiChainTokens && multiChainTokens.length > 1) {
const balancePromises = multiChainTokens.map(async (token) => {
const chain = getChainById(token.chainId);
const chainClient = getPublicClient(chain);
const balance = await chainClient.readContract({
abi: config.contracts.token.abi,
address: token.address,
functionName: "balanceOf",
args: [address],
});
return balance;
});
const balances = await Promise.all(balancePromises);
totalBalance = balances.reduce((sum, bal) => sum + bal, BigInt(0));
}
Configuration Example
// Multi-chain token setup in UI config
tokens: [
{
name: "TOWNS",
symbol: "TOWNS",
address: "0x...", // Base address
chainId: 8453,
decimals: 18,
},
{
name: "TOWNS",
symbol: "TOWNS",
address: "0x...", // Ethereum address
chainId: 1,
decimals: 18,
}
]
Voting power from all chains is automatically aggregated. Users see their total power across all configured chains.
Delegation Actions
Direct Delegation
Delegate directly from a connected wallet:
import { useWriteContract } from "wagmi";
const { writeContract } = useWriteContract();
await writeContract({
address: tokenAddress,
abi: tokenAbi,
functionName: "delegate",
args: [delegateeAddress],
});
Gasless Delegation
Delegate without paying gas using EIP-712 signatures:
// User signs delegation message
const signature = await signTypedData({
domain: {
name: tokenName,
version: "1",
chainId,
verifyingContract: tokenAddress,
},
types: {
Delegation: [
{ name: "delegatee", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "expiry", type: "uint256" },
],
},
primaryType: "Delegation",
message: {
delegatee,
nonce,
expiry,
},
});
// Submit to relay API
await fetch("/api/v1/relay/delegate", {
method: "POST",
body: JSON.stringify({ signature, delegatee, nonce, expiry }),
});
See Gasless Transactions for relay implementation.
Voting Power Display
Format voting power for user interfaces:
// src/lib/votingPowerUtils.ts:132
export function formatVotingPower(
votingPower: bigint,
decimals: number = 18
): number {
const divisor = BigInt(10 ** decimals);
return Number(votingPower / divisor);
}
// With decimal precision
export function formatVotingPowerString(
votingPower: bigint,
decimals: number = 18,
maxDecimals: number = 2
): string {
const divisor = BigInt(10 ** decimals);
const wholePart = votingPower / divisor;
const fractionalPart = votingPower % divisor;
if (fractionalPart === BigInt(0)) {
return wholePart.toString();
}
const fractionalDivisor = BigInt(10 ** (decimals - maxDecimals));
const roundedFractional = fractionalPart / fractionalDivisor;
const fractionalStr = roundedFractional
.toString()
.padStart(maxDecimals, "0");
return `${wholePart}.${fractionalStr}`;
}
Delegation Tracking
Track delegation history and current delegations:
// Query current delegation
const delegatee = await prisma.delegation.findFirst({
where: {
from_address: userAddress,
dao_slug: tenant.slug,
},
orderBy: { block_number: "desc" },
});
// Delegation event structure
interface Delegation {
from: string; // Delegator address
to: string; // Delegatee address
amount?: bigint; // For partial delegation
block_number: number;
timestamp: Date;
}
Self-Delegation
Users must delegate to themselves to activate voting power:
// Check if user needs to self-delegate
const votes = await tokenContract.getVotes(address);
const balance = await tokenContract.balanceOf(address);
if (votes === 0n && balance > 0n) {
// User has tokens but hasn't delegated - prompt self-delegation
await tokenContract.delegate(address);
}
Token holders must delegate (even to themselves) to activate their voting power. Undelegated tokens cannot vote.
Checkpointing
Voting power is checkpointed at the proposal creation block:
// Voting power is snapshot at proposal start
const votingPower = await tokenContract.getPastVotes(
voterAddress,
proposalSnapshot
);
This prevents “double voting” by:
- Locking voting power at proposal creation time
- Preventing transfers from affecting active votes
- Enabling vote replay protection
Token Decimals
Handle different decimal precisions:
const token = Tenant.current().token;
// Most governance tokens use 18 decimals
const decimals = token.decimals || 18;
// Convert between units
const amount = parseUnits("100", decimals); // 100 tokens -> wei
const display = formatUnits(amount, decimals); // wei -> "100"
Advanced Features
Voting Power Sources
Some DAOs calculate voting power from multiple sources:
// Example: Syndicate combines multiple sources
interface VotingPowerSources {
tokenDelegation: bigint; // Delegated tokens
lpPositions: bigint; // Liquidity pool positions
stakedBalances: bigint; // Staked tokens
}
const totalVP = Object.values(sources).reduce(
(sum, vp) => sum + vp,
BigInt(0)
);
Enable via toggle:
const includeAlternativeSources =
ui.toggle("include-nonivotes")?.enabled ?? false;
Delegation Encouragement
Prompt users to delegate if they haven’t:
const showDelegationCTA =
ui.toggle("delegation-encouragement")?.enabled ?? false;
if (showDelegationCTA && !hasDelegated) {
// Show delegation prompt
}
Best Practices
- Cache voting power - Use React Query or SWR to cache contract calls
- Handle zero decimals - Some tokens use fewer than 18 decimals
- Show delegation status - Make it clear who users are delegating to
- Checkpoint awareness - Display voting power at proposal snapshot time
- Multi-chain UX - Clearly indicate which chains contribute to total VP