Разработка мобильного приложения для DAO (голосование, предложения)
DAO-приложение — это интерфейс к управляющему смарт-контракту. Держатели токенов создают предложения (proposals), голосуют, делегируют голоса, следят за исполнением. В отличие от NFT или DeFi, здесь UX ориентирован на чтение: большинство пользователей голосует, а не создаёт предложения. Значит, экран списка предложений — главная точка входа.
Стандарты: OpenZeppelin Governor vs. Snapshot
Два принципиально разных подхода:
On-chain (Governor): всё происходит в блокчейне. OpenZeppelin Governor — стандарт де-факто. Голосование стоит газ, но результаты — криптографически верифицируемые. GovernorBravo, GovernorAlpha — форки использует Compound, Uniswap, Aave.
Off-chain (Snapshot): подписи EIP-712 без транзакций. Голосование бесплатно, хранится в сети Snapshot. Результаты не исполняются автоматически — нужен multisig или исполнитель.
Большинство DAO комбинируют: Snapshot для голосования (без газа), Governor для исполнения прошедших предложений. Приложение должно работать с обоими.
Список предложений: данные и состояния
Предложение проходит через несколько состояний:
| Состояние | Governor | Описание |
|---|---|---|
| Pending | 0 | Ещё не началось голосование |
| Active | 1 | Голосование открыто |
| Canceled | 2 | Отменено автором |
| Defeated | 3 | Не набрало кворум или большинство |
| Succeeded | 4 | Прошло, ожидает постановки в очередь |
| Queued | 5 | В TimeLock, ожидает исполнения |
| Expired | 6 | Истёк дедлайн исполнения |
| Executed | 7 | Исполнено |
Данные через IGovernor.state(proposalId), список предложений — через The Graph или Tally API. Прямые вызовы контракта для списка неэффективны: нет нативного метода getProposals().
// iOS — запрос предложений через Tally GraphQL API
struct TallyProposalsQuery: Codable {
// GraphQL query для получения предложений DAO
static let query = """
query Proposals($governorId: ID!, $first: Int!) {
proposals(
governorId: $governorId,
pagination: { first: $first },
sort: { sortBy: id, isDescending: true }
) {
id
title
description
status
voteStats { support votes percent }
start { ... on Block { timestamp } }
end { ... on Block { timestamp } }
}
}
"""
}
Tally, Boardroom, Messari Governance — агрегаторы с API для большинства крупных DAO. Для собственного DAO — The Graph с субграфом OpenZeppelin Governor.
Экран предложения
Структура экрана:
- Заголовок и описание (Markdown → отрендеренный текст)
- Статус и временные рамки (начало, конец голосования, таймер)
- Результаты в реальном времени: For / Against / Abstain с процентами и прогресс-барами
- Кворум: набрано X из Y голосов (прогресс к порогу)
- Кнопки голосования (только в статусе Active)
- История событий: кто проголосовал, когда
// Android — отображение результатов голосования
data class ProposalVoteStats(
val forVotes: BigDecimal,
val againstVotes: BigDecimal,
val abstainVotes: BigDecimal
) {
val totalVotes get() = forVotes + againstVotes + abstainVotes
val forPercent get() = if (totalVotes > BigDecimal.ZERO)
(forVotes / totalVotes * BigDecimal(100)).toInt() else 0
val quorumReached get() = totalVotes >= quorumThreshold
}
Голосование: on-chain и off-chain (Snapshot)
On-chain: governor.castVote(proposalId, support) — три значения support: 0 (Against), 1 (For), 2 (Abstain). С reason: castVoteWithReason(proposalId, support, reason). Транзакция — газ.
Snapshot off-chain: подпись сообщения EIP-712, отправка в Snapshot API:
// iOS — голосование через Snapshot API (без транзакции)
struct SnapshotVotePayload: Codable {
let version: String // "0.1.3"
let timestamp: Int
let space: String // ID пространства DAO
let type: String // "single-choice", "approval", "quadratic"
let payload: VotePayload
struct VotePayload: Codable {
let proposal: String // IPFS hash предложения
let choice: Int // 1 = For, 2 = Against
let metadata: String // "{}"
}
}
Подпись через WalletConnect или встроенный кошелёк. Бесплатно — ключевое преимущество Snapshot.
Делегирование голосов
Многие токены поддерживают делегирование (ERC20Votes): держатель может передать свою голосующую силу другому адресу, не отдавая токены.
token.delegate(delegateeAddress) — одна транзакция. Пока не вызвана — токены не участвуют в голосовании Governor, даже если они на балансе.
UX: при первом входе в DAO-приложение — проверить, делегированы ли токены. Если нет — баннер «Активируйте право голоса: делегируйте токены (себе или другому)».
// iOS — проверка делегирования
func getDelegatee(for address: EthereumAddress) async throws -> EthereumAddress {
return try await governanceToken.delegates(account: address)
}
// Если результат == .zero или == address — ещё не делегировано
Push-уведомления для активных участников
- Новое предложение создано
- Голосование скоро закрывается (за 24 часа)
- Предложение прошло / отклонено
- Предложение поставлено на исполнение (Queued)
- Кворум набран
Пользователь должен настраивать, о чём получать уведомления — не спамить по всем событиям.
Создание предложения
Создание предложения через Governor — сложная операция: нужно указать targets (адреса контрактов для вызова), values (ETH), calldatas (закодированные вызовы функций) и description. Интерфейс должен либо упростить это через шаблоны, либо предоставить конструктор вызовов для технически грамотных пользователей.
Минимальный порог токенов для создания предложения (proposalThreshold) — проверяй заранее и показывай, достаточно ли у пользователя токенов.
Сроки
| Компонент | Срок |
|---|---|
| Список предложений + статусы | 1 неделя |
| Экран предложения с real-time голосами | 1 неделя |
| On-chain голосование (Governor) | 3 дня |
| Snapshot голосование | 3 дня |
| Делегирование токенов | 2 дня |
| Push-уведомления | 3 дня |
| Создание предложения | 1 неделя |
MVP (список + голосование + делегирование): 3–4 недели. Полное приложение с созданием предложений, историей, аналитикой: 8–12 недель.







