Общие данные
Ссылка: https://github.com/tonwhales/nominators-dao Лицензия: MIT Язык: TactЧто такое DAO-контракт
Это DAO-смарт-контракт, который автоматически:- хранит общий “банк” средств,
- распределяет входящий доход между участниками по заранее заданным долям,
- дает участникам возможность голосовать по важным действиям и предложениям,
- управляет внешним стейкинг-контрактом или пулом, через который формируется доход.
Как это работает
- Фиксированные доли участников
- Общий банк и минимальный баланс
- Голосование по предложениям
- Для принятия решения определенное количество участников должно проголосовать за или против, обычно это около двух третей, но не менее двух голосов.
- Один участник — один голос по каждому конкретному предложению.
- Если набрано достаточно голосов “за”, контракт автоматически выполняет действия, заложенные в предложение. Если набрано достаточно голосов “против”, предложение считается отклоненным.
- Не доверие людям, а доверие к коду
Что контракт дает партнерам
- Прозрачное и автоматическое распределение доходов между всеми участниками по четким правилам.
- Формализованное принятие решений: любые важные действия проходят через голосование, результат которого нельзя “подкрутить”.
- Минимальную операционную нагрузку: не нужны ручные выплаты и ручной контроль, все реализовано в коде.
- Предсказуемость и устойчивость: контракт защищен от “обнуления” за счет минимального баланса и проверок баланса перед рискованными действиями.
Структура и основные сущности
DAOWithSplitter — основной DAO-контракт.Хранит:
managable: Address— адрес управляемого стейкинг-контракта или пула.members: map[Int]Int— набор участников:- Ключ — хэш адреса участника, только basechain,
workchain = 0 - Значение — вес или доля участника в виде целого числа
- Ключ — хэш адреса участника, только basechain,
denominator: Int— общий знаменатель для расчета долей, сумма всех весов участников не превышает это значение.withdrawFee: Int— фиксированная сумма, отправляемая вmanagableпри запросе на вывод стейка.
Хранит:
owner: Address— адрес DAO, то есть инициатор и владелец предложения.members: map[Int]Int— локальную копию долей участников на момент создания предложения.votesNeeded: Int— количество голосов, необходимое для принятия решения.agreed, disagreed— счетчики голосов “за” и “против”.proposal: map[Int]ProposedMessage— набор сообщений, которые будут исполнены в случае принятия предложения.
to: Address, value: Int, mode: Int, bounce: Bool, body: Cell.
Ключевые константы
PROPOSAL_MINIMUM_BALANCE— минимальный баланс, необходимый для безопасного создания предложения.DAO_MINIMUM_BALANCE— минимальный баланс на счете DAO, ниже которого вывод средств недопустим.
Логика голосования и предложений
- Создание предложения, сообщение
CreateProposal- Отправитель должен быть участником DAO, то есть его хэш должен присутствовать в
members. - На счете сообщения
ctx.valueдолжно быть больше, чем2 × PROPOSAL_MINIMUM_BALANCE, чтобы покрыть деплой и работу контракта предложения.
- Отправитель должен быть участником DAO, то есть его хэш должен присутствовать в
- Голосование в контракте
Proposalчерезreceive("Agree")иreceive("Disagree")- Голосовать могут только адреса из basechain, то есть
wc == 0, которые присутствуют вmembersи имеют ненулевой вес. - Один участник может проголосовать только один раз: после голосования его вес в
membersобнуляется. - Если количество голосов “за” больше либо равно
votesNeeded:- Вызывается
executeProposal() - Контракт
Proposalотправляет в DAO сообщениеExecuteProposalс картойmessages.
- Вызывается
- Если количество голосов “против” больше либо равно
votesNeeded:- Вызывается
terminateProposal() - В DAO уходит текстовое сообщение
"Terminated".
- Вызывается
- Голосовать могут только адреса из basechain, то есть
- Исполнение предложения в DAO через
receive(proposal: ExecuteProposal)- DAO проверяет, что отправитель является корректным контрактом предложения.
- Пересчитывает
StateInitожидаемогоProposalна основе текущего состояния DAO и полученной картыmessages. - Сравнивает
ctx.senderс адресом, вычисленным из этогоStateInit.
- Если проверка проходит, DAO последовательно отправляет все
ProposedMessage. - Каждая запись в
messagesстановится обычнымsend()с указанными полями.
Механика распределения
splitAndSend(value: Int)
- Вычисляет
splittable = value / denominator * denominatorи распределяет только эту “делимую” часть. - Если
splittable ≤ 0, ничего не делает. - Итеративно проходит по словарю
membersчерезnativeDictGetMinиnativeDictGetNext. - Для каждого участника:
- извлекает
memberShare, - преобразует хэш-ключ в
Addressbasechain.
- извлекает
- Затем отправляет участнику:
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, поскольку фактический размер стейка определяется внешним контрактом.
- случайным
- DAO отправляет в
"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 — возвращает минимальное число голосов, необходимое для принятия решения.