Skip to main content
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

  1. Cache voting power - Use React Query or SWR to cache contract calls
  2. Handle zero decimals - Some tokens use fewer than 18 decimals
  3. Show delegation status - Make it clear who users are delegating to
  4. Checkpoint awareness - Display voting power at proposal snapshot time
  5. Multi-chain UX - Clearly indicate which chains contribute to total VP

Build docs developers (and LLMs) love