Skip to main content

Общие данные

Ссылка: https://github.com/tonwhales/nominators-dao Лицензия: MIT Язык: Tact

Что такое DAO-контракт

Это DAO-смарт-контракт, который автоматически:
  • хранит общий “банк” средств,
  • распределяет входящий доход между участниками по заранее заданным долям,
  • дает участникам возможность голосовать по важным действиям и предложениям,
  • управляет внешним стейкинг-контрактом или пулом, через который формируется доход.

Как это работает

  • Фиксированные доли участников
Есть список участников с долями, например 10%, 15%, 5% и так далее. Когда в DAO поступают деньги, будь то доход, вывод из пула или другой источник, контракт автоматически распределяет их строго по этим долям. Никакого ручного расчета, кому сколько положено: все зашито в коде.
  • Общий банк и минимальный баланс
Контракт всегда поддерживает минимальный остаток, чтобы не “обнуляться” и продолжать работу. Вывод средств из пула или запуск стейкинговых операций возможен только в том случае, если у DAO достаточно средств.
  • Голосование по предложениям
Любой участник может создать “proposal” — набор действий, которые должен выполнить контракт, например перевод средств, смену адреса управляющего или выполнение технических операций.
  • Для принятия решения определенное количество участников должно проголосовать за или против, обычно это около двух третей, но не менее двух голосов.
  • Один участник — один голос по каждому конкретному предложению.
  • Если набрано достаточно голосов “за”, контракт автоматически выполняет действия, заложенные в предложение. Если набрано достаточно голосов “против”, предложение считается отклоненным.
  • Не доверие людям, а доверие к коду
Все правила, то есть кто является участником, какие у них доли, сколько голосов нужно и как делятся деньги, записаны в смарт-контракте. Их нельзя изменить задним числом без прохождения формальной процедуры, то есть голосования и исполнения предложения. Это снижает риск неформальных договоренностей в обход правил и уменьшает влияние человеческого фактора.

Что контракт дает партнерам

  • Прозрачное и автоматическое распределение доходов между всеми участниками по четким правилам.
  • Формализованное принятие решений: любые важные действия проходят через голосование, результат которого нельзя “подкрутить”.
  • Минимальную операционную нагрузку: не нужны ручные выплаты и ручной контроль, все реализовано в коде.
  • Предсказуемость и устойчивость: контракт защищен от “обнуления” за счет минимального баланса и проверок баланса перед рискованными действиями.

Структура и основные сущности

DAOWithSplitter — основной DAO-контракт.
Хранит:
  • managable: Address — адрес управляемого стейкинг-контракта или пула.
  • members: map[Int]Int — набор участников:
    • Ключ — хэш адреса участника, только basechain, workchain = 0
    • Значение — вес или доля участника в виде целого числа
  • denominator: Int — общий знаменатель для расчета долей, сумма всех весов участников не превышает это значение.
  • withdrawFee: Int — фиксированная сумма, отправляемая в managable при запросе на вывод стейка.
Proposal — вспомогательный контракт для голосования по одному предложению.
Хранит:
  • owner: Address — адрес DAO, то есть инициатор и владелец предложения.
  • members: map[Int]Int — локальную копию долей участников на момент создания предложения.
  • votesNeeded: Int — количество голосов, необходимое для принятия решения.
  • agreed, disagreed — счетчики голосов “за” и “против”.
  • proposal: map[Int]ProposedMessage — набор сообщений, которые будут исполнены в случае принятия предложения.
ProposedMessage — описание одного исходящего сообщения, которое DAO отправит после успешного голосования:
  • to: Address, value: Int, mode: Int, bounce: Bool, body: Cell.

Ключевые константы

  • PROPOSAL_MINIMUM_BALANCE — минимальный баланс, необходимый для безопасного создания предложения.
  • DAO_MINIMUM_BALANCE — минимальный баланс на счете DAO, ниже которого вывод средств недопустим.

Логика голосования и предложений

  1. Создание предложения, сообщение CreateProposal
    1. Отправитель должен быть участником DAO, то есть его хэш должен присутствовать в members.
    2. На счете сообщения ctx.value должно быть больше, чем 2 × PROPOSAL_MINIMUM_BALANCE, чтобы покрыть деплой и работу контракта предложения.
  2. Голосование в контракте Proposal через receive("Agree") и receive("Disagree")
    1. Голосовать могут только адреса из basechain, то есть wc == 0, которые присутствуют в members и имеют ненулевой вес.
    2. Один участник может проголосовать только один раз: после голосования его вес в members обнуляется.
    3. Если количество голосов “за” больше либо равно votesNeeded:
      1. Вызывается executeProposal()
      2. Контракт Proposal отправляет в DAO сообщение ExecuteProposal с картой messages.
    4. Если количество голосов “против” больше либо равно votesNeeded:
      1. Вызывается terminateProposal()
      2. В DAO уходит текстовое сообщение "Terminated".
  3. Исполнение предложения в DAO через receive(proposal: ExecuteProposal)
    1. DAO проверяет, что отправитель является корректным контрактом предложения.
    2. Пересчитывает StateInit ожидаемого Proposal на основе текущего состояния DAO и полученной карты messages.
    3. Сравнивает ctx.sender с адресом, вычисленным из этого StateInit.
  • Если проверка проходит, DAO последовательно отправляет все ProposedMessage.
  • Каждая запись в messages становится обычным send() с указанными полями.

Механика распределения

splitAndSend(value: Int)
  • Вычисляет splittable = value / denominator * denominator и распределяет только эту “делимую” часть.
  • Если splittable ≤ 0, ничего не делает.
  • Итеративно проходит по словарю members через nativeDictGetMin и nativeDictGetNext.
  • Для каждого участника:
    • извлекает memberShare,
    • преобразует хэш-ключ в Address basechain.
  • Затем отправляет участнику:
    • value = splittable / denominator * memberShare
    • в body комментарий вида "X/Y of Z (whales revenue share)", где:
      • X — доля участника,
      • Y — знаменатель,
      • Z — общая распределяемая сумма.

Входящие сообщения в DAO

"Topup DAO" — пополнение DAO без дополнительной логики, сообщение просто принимается.
"Terminated" — служебное сообщение для синхронизации с контрактом предложения, дополнительных действий не выполняется.
"Withdraw" — отправитель должен быть участником DAO.
Проверяется условие: myBalance() - withdrawFee > DAO_MINIMUM_BALANCE.
  • Если условие выполнено:
    • DAO отправляет в managable сообщение WithdrawStake с:
      • случайным queryId,
      • фиксированным gasLimit,
      • stake = 0, поскольку фактический размер стейка определяется внешним контрактом.
"Gift" — любой пользователь может отправить средства с этим текстом.
  • DAO сохраняет DAO_MINIMUM_BALANCE как неснижаемый остаток, а остальную часть, то есть ctx.value - DAO_MINIMUM_BALANCE, если она положительна, распределяет через splitAndSend.
WithdrawStakeResponse и WithdrawStakeDelayed — это сообщения, которые приходят от managable после завершения операций по выводу стейка.
  • По логике они обрабатываются аналогично "Gift":
    • из входящей суммы вычитается DAO_MINIMUM_BALANCE,
    • оставшаяся сумма, если она больше нуля, распределяется между участниками через splitAndSend.

Get-методы

memberShare(addr: Address): Int — возвращает вес участника по его адресу. Если адрес не относится к участникам, выбрасывается ошибка.
membersCount(): Int — считает количество записей в словаре members.
minimumVotes(): Int — возвращает минимальное число голосов, необходимое для принятия решения.